habicron 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/LICENSE +21 -0
- package/README.md +278 -0
- package/dist/browser/index.cjs +1 -0
- package/dist/browser/index.d.cts +42 -0
- package/dist/browser/index.d.mts +42 -0
- package/dist/browser/index.d.ts +42 -0
- package/dist/browser/index.mjs +1 -0
- package/dist/cli/index.cjs +37 -0
- package/dist/cli/index.d.cts +35 -0
- package/dist/cli/index.d.mts +35 -0
- package/dist/cli/index.d.ts +35 -0
- package/dist/cli/index.mjs +37 -0
- package/dist/core/index.cjs +1 -0
- package/dist/core/index.d.cts +137 -0
- package/dist/core/index.d.mts +137 -0
- package/dist/core/index.d.ts +137 -0
- package/dist/core/index.mjs +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.cts +1 -0
- package/dist/node/index.d.mts +1 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.mjs +1 -0
- package/dist/react/index.cjs +1 -0
- package/dist/react/index.d.cts +61 -0
- package/dist/react/index.d.mts +61 -0
- package/dist/react/index.d.ts +61 -0
- package/dist/react/index.mjs +1 -0
- package/dist/vue/index.cjs +1 -0
- package/dist/vue/index.d.cts +84 -0
- package/dist/vue/index.d.mts +84 -0
- package/dist/vue/index.d.ts +84 -0
- package/dist/vue/index.mjs +1 -0
- package/package.json +125 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thecodeorigin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# habicron
|
|
2
|
+
|
|
3
|
+
> Habits, not cronjobs.
|
|
4
|
+
|
|
5
|
+
Schedule callbacks on **randomized recurring intervals**. `habicron` is accurate
|
|
6
|
+
by default (evenly spaced, anchored to the start time, **no drift**) and
|
|
7
|
+
optionally **jittered** — each fire nudged earlier or later within bounds, so
|
|
8
|
+
your jobs run on a human rhythm instead of robotically on the dot.
|
|
9
|
+
|
|
10
|
+
One tiny engine, five entry points:
|
|
11
|
+
|
|
12
|
+
| Import | For | Returns |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| `habicron` / `habicron/node` | Node, workers, scripts | a plain controller |
|
|
15
|
+
| `habicron/browser` | Vanilla browser (no framework) | controller + callbacks |
|
|
16
|
+
| `habicron/vue` | Vue 3 | reactive `ref`s |
|
|
17
|
+
| `habicron/react` | React 17+ | reactive state |
|
|
18
|
+
| `habit` (CLI) | the terminal | runs a shell command |
|
|
19
|
+
|
|
20
|
+
- **No drift** — fires are anchored to a fixed grid; jitter perturbs *around* the
|
|
21
|
+
grid point and never accumulates.
|
|
22
|
+
- **Jitter, bounded** — capped at `0.49 × interval` so adjacent fires can't reorder.
|
|
23
|
+
- **Long delays** — months/years are supported (chunked past `setTimeout`'s 24.8-day ceiling).
|
|
24
|
+
- **Resilient** — a throwing or rejecting callback never kills the schedule.
|
|
25
|
+
- **SSR-safe** — no timers during server render.
|
|
26
|
+
- **Typed** — ships hand-checked `.d.ts`; mutually-exclusive `every` vs `times`/`per`.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm i habicron # or: pnpm add habicron / bun add habicron
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`vue` and `react` are optional peer dependencies — install only the one you use.
|
|
35
|
+
|
|
36
|
+
## Schedule shapes
|
|
37
|
+
|
|
38
|
+
A **habit** is an interval (`every`) or a rate (`times` + `per`), with optional
|
|
39
|
+
`jitter`. Every shape:
|
|
40
|
+
|
|
41
|
+
| Shape | Example | Fires |
|
|
42
|
+
| --- | --- | --- |
|
|
43
|
+
| `{ every: Duration }` | `{ every: '2h' }` | every 2 hours, exactly |
|
|
44
|
+
| `{ every: Duration }` (compound) | `{ every: '1h30m' }` | every 90 minutes |
|
|
45
|
+
| `{ every: '<cadence> ~ <jitter>' }` | `{ every: '2h ~ 5m' }` | every 2h, ± up to 5m (packed; `+/-` also works) |
|
|
46
|
+
| `{ every, jitter: Duration }` | `{ every: '20s', jitter: '5s' }` | every 20s, ± up to 5s |
|
|
47
|
+
| `{ every, jitter: [min, max] }` | `{ every: '20s', jitter: ['3s', '5s'] }` | every 20s, ± 3–5s |
|
|
48
|
+
| `{ every, jitter: { min, max } }` | `{ every: '1h', jitter: { min: '5m', max: '15m' } }` | every 1h, ± 5–15m |
|
|
49
|
+
| `{ times, per }` | `{ times: 2, per: 'day' }` | twice a day, evenly spaced |
|
|
50
|
+
| `{ times, per, jitter }` | `{ times: 2, per: 'day', jitter: '2h' }` | twice a day, ± up to 2h |
|
|
51
|
+
| `{ habits: Schedule[] }` | `{ habits: [a, b, c] }` | the **union** of several habits |
|
|
52
|
+
|
|
53
|
+
Plus the control flags (any shape): `immediate?` (fire once on start),
|
|
54
|
+
`autoStart?` (default `true`), `random?` (seeded RNG), `id?` / `name?` (for the
|
|
55
|
+
registry), and — adapters only — `controls?`.
|
|
56
|
+
|
|
57
|
+
**Durations** are a number (ms) or a string of `<num><unit>` tokens:
|
|
58
|
+
|
|
59
|
+
| Token | Unit | | Token | Unit |
|
|
60
|
+
| --- | --- | --- | --- | --- |
|
|
61
|
+
| `ms` | milliseconds | | `w` | weeks |
|
|
62
|
+
| `s` | seconds | | `mo` | months (avg 30.44 d) |
|
|
63
|
+
| `m` | minutes | | `y` | years (avg 365.25 d) |
|
|
64
|
+
| `h` | hours | | | |
|
|
65
|
+
| `d` | days | | | |
|
|
66
|
+
|
|
67
|
+
**`per`** is one of `minute` `hour` `day` `week` `month` `year`. Jitter sign is
|
|
68
|
+
always random (fires land earlier **or** later), and its magnitude is capped at
|
|
69
|
+
`0.49 × interval` so adjacent fires can't reorder.
|
|
70
|
+
|
|
71
|
+
## Node
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { createHabit } from 'habicron'
|
|
75
|
+
|
|
76
|
+
const job = createHabit(() => syncFeed(), { every: '15m ~ 2m' })
|
|
77
|
+
|
|
78
|
+
job.counter // times fired
|
|
79
|
+
job.nextRun // Date of the next fire, or null
|
|
80
|
+
job.pause()
|
|
81
|
+
job.resume()
|
|
82
|
+
job.stop()
|
|
83
|
+
|
|
84
|
+
process.on('SIGINT', () => { job.stop(); process.exit(0) })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Browser (no framework)
|
|
88
|
+
|
|
89
|
+
Vanilla JS has no refs or state, so reactivity comes through callbacks —
|
|
90
|
+
`onActive`, `onFire`, `onChange`:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { useHabit } from 'habicron/browser'
|
|
94
|
+
|
|
95
|
+
const job = useHabit(() => refreshWidget(), {
|
|
96
|
+
every: '20s ~ 4s',
|
|
97
|
+
onFire: count => (badge.textContent = String(count)),
|
|
98
|
+
onActive: active => dot.classList.toggle('live', active),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
job.pause() // or resume / update / destroy
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`onActive` is the framework-free stand-in for a reactive `isActive`. SSR-safe:
|
|
105
|
+
timers don't start unless a `window` is present.
|
|
106
|
+
|
|
107
|
+
## Vue
|
|
108
|
+
|
|
109
|
+
```vue
|
|
110
|
+
<script setup lang="ts">
|
|
111
|
+
import { useHabit } from 'habicron/vue'
|
|
112
|
+
|
|
113
|
+
const { counter, nextRun, pause, resume } = useHabit(post, {
|
|
114
|
+
controls: true,
|
|
115
|
+
every: '20s ~ 4s',
|
|
116
|
+
})
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<template>
|
|
120
|
+
<p>fired {{ counter }}× · next at {{ nextRun?.toLocaleTimeString() }}</p>
|
|
121
|
+
<button @click="pause">Pause</button>
|
|
122
|
+
</template>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`counter`, `nextRun` and `isActive` are readonly refs. Control members
|
|
126
|
+
(`pause`, `resume`, `reset`, `isActive`) appear only when `controls: true`.
|
|
127
|
+
|
|
128
|
+
## React
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { useHabit } from 'habicron/react'
|
|
132
|
+
|
|
133
|
+
function Reminder() {
|
|
134
|
+
const { counter, nextRun, pause } = useHabit(
|
|
135
|
+
() => notify('Drink water'),
|
|
136
|
+
{ controls: true, every: '1h ~ 8m' },
|
|
137
|
+
)
|
|
138
|
+
return (
|
|
139
|
+
<p>
|
|
140
|
+
fired {counter}× · next at {nextRun?.toLocaleTimeString()}
|
|
141
|
+
<button onClick={pause}>Pause</button>
|
|
142
|
+
</p>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
React returns plain values (not refs). The controller is created inside an
|
|
148
|
+
effect, so it is SSR-safe; the callback is always read fresh.
|
|
149
|
+
|
|
150
|
+
## Multiple habits
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
useHabit(runAgent, {
|
|
154
|
+
controls: true,
|
|
155
|
+
habits: [
|
|
156
|
+
{ every: '2h ~ 20m' }, // check the cat
|
|
157
|
+
{ times: 2, per: 'day', jitter: '90m' }, // twice a day
|
|
158
|
+
{ every: '3d', jitter: ['3h', '5h'] }, // every few days
|
|
159
|
+
],
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The callback fires on the **union** of all habits.
|
|
164
|
+
|
|
165
|
+
## CLI
|
|
166
|
+
|
|
167
|
+
The `habit` command runs any shell command on a randomized schedule. It works
|
|
168
|
+
two ways — attached, or managed by a lightweight background daemon.
|
|
169
|
+
|
|
170
|
+
**Attached** — fires in your terminal until you Ctrl-C:
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
habit run --every "10s ~ 2s" -- echo "stretch"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Managed** — a background daemon keeps habits firing, and you list / inspect /
|
|
177
|
+
update / delete them like processes:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
habit start --name sync --every "1h ~ 5m" -- npm run sync # create + run in background
|
|
181
|
+
habit start --times 3 --per day --jitter 2h -- ./backup.sh
|
|
182
|
+
habit list # what's running, and what it runs
|
|
183
|
+
habit logs sync # recent output
|
|
184
|
+
habit stop sync # pause
|
|
185
|
+
habit start sync # resume
|
|
186
|
+
habit restart sync
|
|
187
|
+
habit update sync --every 30m # change schedule live
|
|
188
|
+
habit delete sync # remove (alias: rm)
|
|
189
|
+
habit kill # stop the daemon
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`habit list` shows each habit's id, name, status, schedule, **the command it
|
|
193
|
+
runs**, fire count, and next/last run:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
id name status schedule command runs next last
|
|
197
|
+
1 sync running every 1h~5m npm run sync 4 in 52m 8m ago
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Habit definitions persist in `~/.habit/` (override with `HABIT_HOME`).
|
|
201
|
+
|
|
202
|
+
| Schedule flag | Meaning |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| `--every <dur>` | interval between fires |
|
|
205
|
+
| `--times <n> --per <period>` | N times per minute…year |
|
|
206
|
+
| `--jitter <dur>` | max random nudge per fire |
|
|
207
|
+
| `-i, --immediate` | fire once immediately |
|
|
208
|
+
| `--name <n>` | label for `list` / `logs` / etc. |
|
|
209
|
+
|
|
210
|
+
## API
|
|
211
|
+
|
|
212
|
+
`createHabit(callback, options)` → `HabitController`
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
interface HabitController {
|
|
216
|
+
readonly counter: number
|
|
217
|
+
readonly isActive: boolean
|
|
218
|
+
readonly nextRun: Date | null
|
|
219
|
+
start: (immediate?: boolean) => void
|
|
220
|
+
stop: () => void
|
|
221
|
+
pause: () => void
|
|
222
|
+
resume: () => void
|
|
223
|
+
reset: () => void // zero counter, restart if active
|
|
224
|
+
subscribe: (listener: () => void) => () => void
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface ControlFlags {
|
|
228
|
+
immediate?: boolean // fire once on start
|
|
229
|
+
autoStart?: boolean // default true (adapters gate this for SSR)
|
|
230
|
+
random?: () => number // inject a seeded RNG for determinism
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The framework adapters wrap this: Vue maps it onto refs, React onto state.
|
|
235
|
+
|
|
236
|
+
### Managing habits
|
|
237
|
+
|
|
238
|
+
Every habit is registered, so you can list, look up, update, and remove them:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
import { createHabit, getHabit, listHabits } from 'habicron'
|
|
242
|
+
|
|
243
|
+
createHabit(syncFeed, { id: 'feed', name: 'Feed sync', every: '15m ~ 2m' })
|
|
244
|
+
|
|
245
|
+
listHabits() // [{ id, name, counter, nextRun, isActive, … }, …]
|
|
246
|
+
const job = getHabit('feed')
|
|
247
|
+
job?.update({ every: '5m' }) // reschedule in place (keeps id + counter)
|
|
248
|
+
job?.destroy() // stop and unregister
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
In Vue and React, `useHabits()` returns that list **reactively** — a ready-made
|
|
252
|
+
management view that updates as habits fire or come and go:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { useHabits } from 'habicron/vue' // or 'habicron/react'
|
|
256
|
+
|
|
257
|
+
const habits = useHabits() // Vue: Ref<HabitSummary[]> · React: HabitSummary[]
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Scope
|
|
261
|
+
|
|
262
|
+
`habicron` is a **client/runtime scheduler**, not a durable job queue. It does
|
|
263
|
+
not provide persistence across reloads, at-least-once delivery, or distributed
|
|
264
|
+
coordination, and browser timers may be throttled in background tabs. For those,
|
|
265
|
+
reach for a server-side scheduler (e.g. a queue or Durable Object alarms).
|
|
266
|
+
|
|
267
|
+
## Develop
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
pnpm install
|
|
271
|
+
pnpm typecheck # tsc --noEmit
|
|
272
|
+
pnpm test # vitest run
|
|
273
|
+
pnpm prepack # unbuild → dist/ (ESM + CJS + .d.ts)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
[MIT](./LICENSE) © thecodeorigin
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";const core_index=require("../core/index.cjs");function c(e){return{id:e.id,name:e.name,isActive:e.isActive,counter:e.counter,nextRun:e.nextRun}}function useHabit(e,i){const{onActive:s,onFire:o,onChange:r}=i,t=core_index.createHabit(e,{...i,autoStart:i.autoStart??typeof window<"u"});let n=t.isActive,a=t.counter;return t.subscribe(()=>{t.isActive!==n&&(n=t.isActive,s?.(t.isActive)),t.counter!==a&&(a=t.counter,o?.(t.counter)),r?.(c(t))}),s?.(t.isActive),r?.(c(t)),t}exports.clearHabits=core_index.clearHabits,exports.createHabit=core_index.createHabit,exports.getHabit=core_index.getHabit,exports.listHabits=core_index.listHabits,exports.subscribeHabits=core_index.subscribeHabits,exports.useHabit=useHabit;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { HabitSummary, HabitOptions, HabitController } from '../core/index.cjs';
|
|
2
|
+
export { ControlFlags, Duration, Jitter, Period, Schedule, clearHabits, createHabit, getHabit, listHabits, subscribeHabits } from '../core/index.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* habicron — browser adapter.
|
|
6
|
+
*
|
|
7
|
+
* Framework-agnostic reactivity for plain browser apps. Vanilla JS has no refs
|
|
8
|
+
* or component state, so habicron delivers state changes through callbacks
|
|
9
|
+
* instead: `onActive` (running state flipped), `onFire` (fired), and `onChange`
|
|
10
|
+
* (any change, with a snapshot). The scheduler is the same core engine.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* import { useHabit } from 'habicron/browser'
|
|
14
|
+
*
|
|
15
|
+
* useHabit(() => refreshWidget(), {
|
|
16
|
+
* every: '20s ~ 4s',
|
|
17
|
+
* onFire: count => badge.textContent = String(count),
|
|
18
|
+
* onActive: active => dot.classList.toggle('live', active),
|
|
19
|
+
* })
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface HabitCallbacks {
|
|
23
|
+
/** Called when the running state flips, and once on creation. */
|
|
24
|
+
onActive?: (isActive: boolean) => void;
|
|
25
|
+
/** Called after each fire, with the new total count. */
|
|
26
|
+
onFire?: (counter: number) => void;
|
|
27
|
+
/** Called on any state change, with a plain snapshot. */
|
|
28
|
+
onChange?: (summary: HabitSummary) => void;
|
|
29
|
+
}
|
|
30
|
+
/** Schedule + control flags, plus the browser reactivity callbacks. */
|
|
31
|
+
type UseHabitOptions = HabitOptions & HabitCallbacks;
|
|
32
|
+
/**
|
|
33
|
+
* Create a habit and wire its state changes to callbacks. Returns the
|
|
34
|
+
* {@link HabitController} so you can `pause`/`resume`/`update`/`destroy` it.
|
|
35
|
+
*
|
|
36
|
+
* SSR-safe: timers don't start unless a `window` is present (override with
|
|
37
|
+
* `autoStart`).
|
|
38
|
+
*/
|
|
39
|
+
declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions): HabitController;
|
|
40
|
+
|
|
41
|
+
export { HabitController, HabitOptions, HabitSummary, useHabit };
|
|
42
|
+
export type { HabitCallbacks, UseHabitOptions };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { HabitSummary, HabitOptions, HabitController } from '../core/index.mjs';
|
|
2
|
+
export { ControlFlags, Duration, Jitter, Period, Schedule, clearHabits, createHabit, getHabit, listHabits, subscribeHabits } from '../core/index.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* habicron — browser adapter.
|
|
6
|
+
*
|
|
7
|
+
* Framework-agnostic reactivity for plain browser apps. Vanilla JS has no refs
|
|
8
|
+
* or component state, so habicron delivers state changes through callbacks
|
|
9
|
+
* instead: `onActive` (running state flipped), `onFire` (fired), and `onChange`
|
|
10
|
+
* (any change, with a snapshot). The scheduler is the same core engine.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* import { useHabit } from 'habicron/browser'
|
|
14
|
+
*
|
|
15
|
+
* useHabit(() => refreshWidget(), {
|
|
16
|
+
* every: '20s ~ 4s',
|
|
17
|
+
* onFire: count => badge.textContent = String(count),
|
|
18
|
+
* onActive: active => dot.classList.toggle('live', active),
|
|
19
|
+
* })
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface HabitCallbacks {
|
|
23
|
+
/** Called when the running state flips, and once on creation. */
|
|
24
|
+
onActive?: (isActive: boolean) => void;
|
|
25
|
+
/** Called after each fire, with the new total count. */
|
|
26
|
+
onFire?: (counter: number) => void;
|
|
27
|
+
/** Called on any state change, with a plain snapshot. */
|
|
28
|
+
onChange?: (summary: HabitSummary) => void;
|
|
29
|
+
}
|
|
30
|
+
/** Schedule + control flags, plus the browser reactivity callbacks. */
|
|
31
|
+
type UseHabitOptions = HabitOptions & HabitCallbacks;
|
|
32
|
+
/**
|
|
33
|
+
* Create a habit and wire its state changes to callbacks. Returns the
|
|
34
|
+
* {@link HabitController} so you can `pause`/`resume`/`update`/`destroy` it.
|
|
35
|
+
*
|
|
36
|
+
* SSR-safe: timers don't start unless a `window` is present (override with
|
|
37
|
+
* `autoStart`).
|
|
38
|
+
*/
|
|
39
|
+
declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions): HabitController;
|
|
40
|
+
|
|
41
|
+
export { HabitController, HabitOptions, HabitSummary, useHabit };
|
|
42
|
+
export type { HabitCallbacks, UseHabitOptions };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { HabitSummary, HabitOptions, HabitController } from '../core/index.js';
|
|
2
|
+
export { ControlFlags, Duration, Jitter, Period, Schedule, clearHabits, createHabit, getHabit, listHabits, subscribeHabits } from '../core/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* habicron — browser adapter.
|
|
6
|
+
*
|
|
7
|
+
* Framework-agnostic reactivity for plain browser apps. Vanilla JS has no refs
|
|
8
|
+
* or component state, so habicron delivers state changes through callbacks
|
|
9
|
+
* instead: `onActive` (running state flipped), `onFire` (fired), and `onChange`
|
|
10
|
+
* (any change, with a snapshot). The scheduler is the same core engine.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* import { useHabit } from 'habicron/browser'
|
|
14
|
+
*
|
|
15
|
+
* useHabit(() => refreshWidget(), {
|
|
16
|
+
* every: '20s ~ 4s',
|
|
17
|
+
* onFire: count => badge.textContent = String(count),
|
|
18
|
+
* onActive: active => dot.classList.toggle('live', active),
|
|
19
|
+
* })
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface HabitCallbacks {
|
|
23
|
+
/** Called when the running state flips, and once on creation. */
|
|
24
|
+
onActive?: (isActive: boolean) => void;
|
|
25
|
+
/** Called after each fire, with the new total count. */
|
|
26
|
+
onFire?: (counter: number) => void;
|
|
27
|
+
/** Called on any state change, with a plain snapshot. */
|
|
28
|
+
onChange?: (summary: HabitSummary) => void;
|
|
29
|
+
}
|
|
30
|
+
/** Schedule + control flags, plus the browser reactivity callbacks. */
|
|
31
|
+
type UseHabitOptions = HabitOptions & HabitCallbacks;
|
|
32
|
+
/**
|
|
33
|
+
* Create a habit and wire its state changes to callbacks. Returns the
|
|
34
|
+
* {@link HabitController} so you can `pause`/`resume`/`update`/`destroy` it.
|
|
35
|
+
*
|
|
36
|
+
* SSR-safe: timers don't start unless a `window` is present (override with
|
|
37
|
+
* `autoStart`).
|
|
38
|
+
*/
|
|
39
|
+
declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions): HabitController;
|
|
40
|
+
|
|
41
|
+
export { HabitController, HabitOptions, HabitSummary, useHabit };
|
|
42
|
+
export type { HabitCallbacks, UseHabitOptions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHabit as s}from"../core/index.mjs";export{clearHabits,getHabit,listHabits,subscribeHabits}from"../core/index.mjs";function u(e){return{id:e.id,name:e.name,isActive:e.isActive,counter:e.counter,nextRun:e.nextRun}}function b(e,i){const{onActive:n,onFire:a,onChange:r}=i,t=s(e,{...i,autoStart:i.autoStart??typeof window<"u"});let c=t.isActive,o=t.counter;return t.subscribe(()=>{t.isActive!==c&&(c=t.isActive,n?.(t.isActive)),t.counter!==o&&(o=t.counter,a?.(t.counter)),r?.(u(t))}),n?.(t.isActive),r?.(u(t)),t}export{s as createHabit,b as useHabit};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";const node_child_process=require("node:child_process"),node_fs=require("node:fs"),u$1=require("node:process"),core_index=require("../core/index.cjs"),node_path=require("node:path"),node_os=require("node:os");function _interopDefaultCompat(e){return e&&typeof e=="object"&&"default"in e?e.default:e}const u__default=_interopDefaultCompat(u$1);function habitHome(){return u__default.env.HABIT_HOME??node_path.join(node_os.homedir(),".habit")}const p$1=()=>node_path.join(habitHome(),"habits.json"),g=()=>node_path.join(habitHome(),"state.json"),u=()=>node_path.join(habitHome(),"daemon.json"),logFile=e=>node_path.join(habitHome(),"logs",`${e}.log`);function d$1(e,t){try{return JSON.parse(node_fs.readFileSync(e,"utf8"))}catch{return t}}function m(e,t){node_fs.mkdirSync(habitHome(),{recursive:!0}),node_fs.writeFileSync(e,`${JSON.stringify(t,null,2)}
|
|
3
|
+
`)}function loadHabits(){return d$1(p$1(),[])}function saveHabits(e){m(p$1(),e)}function findHabit(e,t){return e.find(n=>n.id===t)??e.find(n=>n.name===t)}function j$1(e){const t=e.reduce((n,r)=>Math.max(n,Number(r.id)||0),0);return String(t+1)}function defaultName(e){const t=e.find(n=>/\.(?:[cm]?js|ts|sh|py|rb)$/.test(n))??e[0]??"habit";return node_path.basename(t).replace(/\.[^.]+$/,"")}function addHabit(e){const t=loadHabits(),n={id:j$1(t),name:e.name??defaultName(e.command),command:e.command,every:e.every,times:e.times,per:e.per,jitter:e.jitter,immediate:e.immediate,status:"running",rev:0,createdAt:new Date().toISOString()};return t.push(n),saveHabits(t),n}function patchHabit(e,t){const n=loadHabits(),r=findHabit(n,e);if(r)return Object.assign(r,t),saveHabits(n),r}function removeHabit(e){const t=loadHabits(),n=findHabit(t,e);if(!n)return;saveHabits(t.filter(i=>i!==n));const r=loadState();return delete r[n.id],saveState(r),n}function loadState(){return d$1(g(),{})}function saveState(e){m(g(),e)}const w={counter:0,lastRun:null,lastExit:null,nextRun:null,startedAt:null};function patchState(e,t){const n=loadState();n[e]={...w,...n[e],...t},saveState(n)}function readDaemon(){return d$1(u(),null)}function writeDaemon(e){m(u(),e)}function clearDaemon(){node_fs.existsSync(u())&&m(u(),null)}function daemonAlive(){const e=readDaemon();if(e?.pid==null)return!1;try{return u__default.kill(e.pid,0),!0}catch{return!1}}function recordToOptions(e){const t={id:e.id,name:e.name,immediate:e.immediate,autoStart:!1};if(e.every!=null)return{...t,every:e.every,...e.jitter!=null?{jitter:e.jitter}:{}};if(e.times!=null&&e.per!=null)return{...t,times:e.times,per:e.per,...e.jitter!=null?{jitter:e.jitter}:{}};throw new Error(`habit "${e.id}" has no schedule (every, or times + per)`)}function scheduleLabel(e){if(e.every!=null)return`every ${e.every}`;const t=`${e.times}\xD7/${e.per}`;return e.jitter!=null?`${t} ~ ${e.jitter}`:t}function $$1(e){if(e==null)return"\u2014";const t=Date.now()-new Date(e).getTime();return t<0?`in ${f(-t)}`:`${f(t)} ago`}function f(e){const t=Math.round(e/1e3);if(t<60)return`${t}s`;const n=Math.round(t/60);if(n<60)return`${n}m`;const r=Math.round(n/60);return r<24?`${r}h`:`${Math.round(r/24)}d`}function S(e,t){if(t==="stopped"||e==null)return"\u2014";const n=new Date(e).getTime()-Date.now();return n<=0?"now":`in ${f(n)}`}function formatList(e,t){if(e.length===0)return'No habits yet. Create one with: habit start --every "1h ~ 5m" -- <command>';const n=["id","name","status","schedule","command","runs","next","last"],r=e.map(c=>{const s=t[c.id];return[c.id,c.name,c.status,scheduleLabel(c),c.command.join(" "),String(s?.counter??0),S(s?.nextRun??null,c.status),$$1(s?.lastRun??null)]}),i=n.map((c,s)=>Math.max(c.length,...r.map(b=>b[s].length))),l=c=>c.map((s,b)=>s.padEnd(i[b])).join(" ").trimEnd();return[l(n),l(i.map(c=>"\u2500".repeat(c))),...r.map(l)].join(`
|
|
4
|
+
`)}function d(e,t){const n=logFile(e);node_fs.mkdirSync(node_path.dirname(n),{recursive:!0}),node_fs.appendFileSync(n,t)}async function x$1(e){return new Promise(t=>{const n=new Date;d(e.id,`
|
|
5
|
+
[${n.toISOString()}] $ ${e.command.join(" ")}
|
|
6
|
+
`);let r;try{const[i,...l]=e.command;r=node_child_process.spawn(i,l,{stdio:["ignore","pipe","pipe"],shell:!1})}catch(i){const l=i instanceof Error?i.message:String(i);d(e.id,`[habit] spawn failed: ${l}
|
|
7
|
+
`),patchState(e.id,{lastRun:n.toISOString(),lastExit:null}),t();return}r.stdout?.on("data",i=>d(e.id,i)),r.stderr?.on("data",i=>d(e.id,i)),r.on("error",i=>{d(e.id,`[habit] ${i.message}
|
|
8
|
+
`),patchState(e.id,{lastRun:n.toISOString(),lastExit:null}),t()}),r.on("close",i=>{patchState(e.id,{lastRun:n.toISOString(),lastExit:i}),t()})})}function runDaemon(){writeDaemon({pid:u__default.pid,startedAt:new Date().toISOString()});const e=new Map,t=s=>{const b=core_index.createHabit(async()=>x$1(s),recordToOptions(s));return b.subscribe(()=>{patchState(s.id,{counter:b.counter,nextRun:b.nextRun?.toISOString()??null})}),patchState(s.id,{startedAt:new Date().toISOString()}),b.start(s.immediate??!1),{ctrl:b,rev:s.rev}},n=s=>{const b=e.get(s);b&&(b.ctrl.destroy(),e.delete(s),patchState(s,{nextRun:null,startedAt:null}))},r=()=>{const s=loadHabits(),b=new Set(s.map(v=>v.id));for(const v of[...e.keys()])b.has(v)||n(v);for(const v of s){const y=e.get(v.id);if(v.status!=="running"){y&&n(v.id);continue}y?y.rev!==v.rev&&(n(v.id),e.set(v.id,t(v))):e.set(v.id,t(v))}};r();const i=setInterval(r,1e3),l=()=>{clearInterval(i);for(const s of[...e.keys()])n(s);clearDaemon(),u__default.exit(0)};u__default.on("SIGTERM",l),u__default.on("SIGINT",l);const c=loadState();for(const s of Object.keys(c))e.has(s)||patchState(s,{nextRun:null,startedAt:null})}const VERSION="0.2.0",$=["minute","hour","day","week","month","year"],C=$;function A(e){return e!=null&&C.includes(e)}function parseArgs(e){const t={immediate:!1,help:!1,version:!1,command:[]};for(let n=0;n<e.length;n++){const r=e[n];if(r==="--"){t.command=e.slice(n+1);break}switch(r){case"-h":case"--help":t.help=!0;break;case"-v":case"--version":t.version=!0;break;case"-i":case"--immediate":t.immediate=!0;break;case"--name":t.name=e[++n];break;case"--every":t.every=e[++n];break;case"--times":{const i=Number(e[++n]);if(!Number.isFinite(i)||i<=0)return{error:`--times expects a positive number, got "${e[n]}"`};t.times=i;break}case"--per":{const i=e[++n];if(!A(i))return{error:`--per expects one of ${$.join(", ")}`};t.per=i;break}case"--jitter":t.jitter=e[++n];break;case"--max":{const i=Number(e[++n]);if(!Number.isFinite(i)||i<=0)return{error:`--max expects a positive number, got "${e[n]}"`};t.max=i;break}case"--exec":t.command=e.slice(n+1),n=e.length;break;default:if(r.startsWith("-"))return{error:`unknown option "${r}"`};t.command=e.slice(n),n=e.length}}return{args:t}}function toOptions(e){let t=null;return e.every!=null?t={every:e.every,...e.jitter!=null?{jitter:e.jitter}:{}}:e.times!=null&&e.per!=null&&(t={times:e.times,per:e.per,...e.jitter!=null?{jitter:e.jitter}:{}}),t?{options:{...t,immediate:e.immediate,autoStart:!1}}:{error:"a schedule is required: use --every <dur> or --times <n> --per <period>"}}function D(e){return e.command.length===0?{error:'no command given. Pass one after "--".'}:e.every==null&&(e.times==null||e.per==null)?{error:"a schedule is required: --every <dur> or --times <n> --per <period>"}:{habit:{name:e.name,command:e.command,every:e.every,times:e.times,per:e.per,jitter:e.jitter,immediate:e.immediate}}}const HELP=`habit v${VERSION} \u2014 randomized recurring schedules ("habits", not cronjobs)
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
habit start [--name <n>] <schedule> -- <command...> create + run in the background
|
|
12
|
+
habit run <schedule> -- <command...> run attached (Ctrl-C to stop)
|
|
13
|
+
habit list list habits and what they run
|
|
14
|
+
habit stop <id|name|all> pause
|
|
15
|
+
habit start <id|name> resume a paused habit
|
|
16
|
+
habit restart <id|name> restart
|
|
17
|
+
habit update <id|name> [--every \u2026 | --name \u2026 | -- <command...>]
|
|
18
|
+
habit delete <id|name|all> remove (alias: rm)
|
|
19
|
+
habit logs <id|name> [-n <lines>] show recent output
|
|
20
|
+
habit kill stop the background daemon
|
|
21
|
+
|
|
22
|
+
Schedule:
|
|
23
|
+
--every <dur> interval, e.g. "2h", "10s ~ 2s", "1h30m"
|
|
24
|
+
--times <n> --per <period> N times per minute|hour|day|week|month|year
|
|
25
|
+
--jitter <dur> max random nudge per fire, e.g. "5m"
|
|
26
|
+
-i, --immediate fire once immediately on start
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
habit start --every "10s ~ 2s" -- echo "stretch"
|
|
30
|
+
habit start --name sync --times 3 --per hour --jitter 5m -- npm run sync
|
|
31
|
+
habit list
|
|
32
|
+
`;function a(e){u__default.stdout.write(`${e}
|
|
33
|
+
`)}function o(e){u__default.stderr.write(`[habit] ${e}
|
|
34
|
+
`)}function h(){if(daemonAlive())return;const e=u__default.argv[1];node_child_process.spawn(u__default.execPath,[e,"__daemon"],{detached:!0,stdio:"ignore"}).unref()}function p(e){return`${e.id} (${e.name}) \u2192 ${e.command.join(" ")}`}function I(e){if(e.length===1&&!e[0].startsWith("-")){const c=findHabit(loadHabits(),e[0]);if(c)return patchHabit(c.id,{status:"running",rev:c.rev+1}),h(),a(`resumed ${p(c)}`),0}const{args:t,error:n}=parseArgs(e);if(n!=null||!t)return o(n??"parse error"),1;const{habit:r,error:i}=D(t);if(i!=null||!r)return o(i??"invalid habit"),1;const l=addHabit(r);return h(),a(`started ${p(l)}`),a(` ${t.every!=null?`every ${t.every}`:`${t.times}\xD7/${t.per}`}${t.jitter!=null?` ~ ${t.jitter}`:""}`),0}function x(e,t){const n=loadHabits();let r;if(e==="all")r=n;else{const i=findHabit(n,e);r=i?[i]:[]}if(r.length===0)return o(`no habit matching "${e}"`),1;for(const i of r)t(i);return 0}function T(e){return e[0]?x(e[0],t=>{patchHabit(t.id,{status:"stopped"}),a(`stopped ${t.id} (${t.name})`)}):(o("usage: habit stop <id|name|all>"),1)}function L(e){if(!e[0])return o("usage: habit restart <id|name|all>"),1;const t=x(e[0],n=>{patchHabit(n.id,{status:"running",rev:n.rev+1}),a(`restarted ${n.id} (${n.name})`)});return t===0&&h(),t}function F(e){if(!e[0])return o("usage: habit delete <id|name|all>"),1;if(e[0]==="all"){const n=loadHabits();if(n.length===0)return o("no habits to delete"),1;for(const r of n)removeHabit(r.id);return a(`deleted ${n.length} habit(s)`),0}const t=removeHabit(e[0]);return t?(a(`deleted ${t.id} (${t.name})`),0):(o(`no habit matching "${e[0]}"`),1)}function W(e){const t=e[0];if(!t||t.startsWith("-"))return o("usage: habit update <id|name> [--every \u2026 | --name \u2026 | -- <command...>]"),1;const n=findHabit(loadHabits(),t);if(!n)return o(`no habit matching "${t}"`),1;const{args:r,error:i}=parseArgs(e.slice(1));if(i!=null||!r)return o(i??"parse error"),1;const l={rev:n.rev+1};r.name!=null&&(l.name=r.name),r.every!=null&&(l.every=r.every,l.times=void 0,l.per=void 0),r.times!=null&&(l.times=r.times),r.per!=null&&(l.per=r.per),r.jitter!=null&&(l.jitter=r.jitter),r.immediate&&(l.immediate=!0),r.command.length>0&&(l.command=r.command);const c=patchHabit(n.id,l);return c?(h(),a(`updated ${p(c)}`),0):(o(`no habit matching "${t}"`),1)}function _(){a(formatList(loadHabits(),loadState()));const e=readDaemon();return a(""),a(daemonAlive()&&e?`daemon: running (pid ${e.pid})`:"daemon: not running"),0}function M(e){let t,n=50;for(let i=0;i<e.length;i++)e[i]==="-n"?n=Math.max(1,Number(e[++i])||50):!e[i].startsWith("-")&&t==null&&(t=e[i]);if(t==null)return o("usage: habit logs <id|name> [-n <lines>]"),1;const r=findHabit(loadHabits(),t);if(!r)return o(`no habit matching "${t}"`),1;try{const i=node_fs.readFileSync(logFile(r.id),"utf8").trimEnd().split(`
|
|
35
|
+
`).slice(-n).join(`
|
|
36
|
+
`);a(i||"(no output yet)")}catch{a("(no output yet)")}return 0}function O(){const e=readDaemon();if(e?.pid==null||!daemonAlive())return a("daemon: not running"),0;try{u__default.kill(e.pid,"SIGTERM"),a(`stopped daemon (pid ${e.pid})`)}catch(t){return o(`could not stop daemon: ${t instanceof Error?t.message:String(t)}`),1}return 0}async function j(e){const{args:t,error:n}=parseArgs(e);if(n!=null||!t)return o(n??"parse error"),1;if(t.command.length===0)return o('no command given. Pass one after "--".'),1;const{options:r,error:i}=toOptions(t);return i!=null||!r?(o(i??"invalid schedule"),1):(await new Promise(l=>{const c=core_index.createHabit(async()=>{const b=new Date().toLocaleTimeString();return a(`[habit] ${b} \u2192 ${t.command.join(" ")}`),G(t.command)},r);c.start(t.immediate),c.nextRun&&a(`[habit] first run at ${c.nextRun.toLocaleTimeString()}`);const s=()=>{c.stop(),a(`
|
|
37
|
+
[habit] stopped`),l()};u__default.on("SIGINT",s),u__default.on("SIGTERM",s)}),0)}async function G(e){return new Promise(t=>{const[n,...r]=e,i=node_child_process.spawn(n,r,{stdio:"inherit",shell:!1});i.on("error",l=>o(`command failed: ${l.message}`)),i.on("close",()=>t())})}async function main(e){const[t,...n]=e;if(t==null||t==="-h"||t==="--help")return u__default.stdout.write(HELP),0;if(t==="-v"||t==="--version")return a(VERSION),0;switch(t){case"__daemon":return runDaemon(),new Promise(()=>{});case"run":return j(n);case"start":return I(n);case"stop":return T(n);case"restart":return L(n);case"update":return W(n);case"delete":case"rm":return F(n);case"list":case"ls":return _();case"logs":return M(n);case"kill":return O();default:return t.startsWith("-")?j(e):(o(`unknown command "${t}". Try: habit --help`),1)}}const q=typeof u__default<"u"&&u__default.argv[1]!=null&&/habit|cli[\\/]index/.test(u__default.argv[1]);q&&(u__default.stdout.on("error",e=>{e.code==="EPIPE"&&u__default.exit(0)}),main(u__default.argv.slice(2)).then(e=>{u__default.exitCode=e})),exports.HELP=HELP,exports.VERSION=VERSION,exports.main=main,exports.parseArgs=parseArgs,exports.toOptions=toOptions;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Period, Schedule } from '../core/index.cjs';
|
|
2
|
+
|
|
3
|
+
declare const VERSION = "0.2.0";
|
|
4
|
+
interface CliArgs {
|
|
5
|
+
name?: string;
|
|
6
|
+
every?: string;
|
|
7
|
+
times?: number;
|
|
8
|
+
per?: Period;
|
|
9
|
+
jitter?: string;
|
|
10
|
+
immediate: boolean;
|
|
11
|
+
max?: number;
|
|
12
|
+
help: boolean;
|
|
13
|
+
version: boolean;
|
|
14
|
+
command: string[];
|
|
15
|
+
}
|
|
16
|
+
interface ParseResult {
|
|
17
|
+
args?: CliArgs;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Parse schedule flags + command (the bit shared by `run`/`start`/`update`). */
|
|
21
|
+
declare function parseArgs(argv: string[]): ParseResult;
|
|
22
|
+
/** Build core {@link Schedule}-based options for `run` (foreground). */
|
|
23
|
+
declare function toOptions(args: CliArgs): {
|
|
24
|
+
options?: Schedule & {
|
|
25
|
+
immediate?: boolean;
|
|
26
|
+
autoStart?: boolean;
|
|
27
|
+
};
|
|
28
|
+
error?: string;
|
|
29
|
+
};
|
|
30
|
+
declare const HELP = "habit v0.2.0 \u2014 randomized recurring schedules (\"habits\", not cronjobs)\n\nUsage:\n habit start [--name <n>] <schedule> -- <command...> create + run in the background\n habit run <schedule> -- <command...> run attached (Ctrl-C to stop)\n habit list list habits and what they run\n habit stop <id|name|all> pause\n habit start <id|name> resume a paused habit\n habit restart <id|name> restart\n habit update <id|name> [--every \u2026 | --name \u2026 | -- <command...>]\n habit delete <id|name|all> remove (alias: rm)\n habit logs <id|name> [-n <lines>] show recent output\n habit kill stop the background daemon\n\nSchedule:\n --every <dur> interval, e.g. \"2h\", \"10s ~ 2s\", \"1h30m\"\n --times <n> --per <period> N times per minute|hour|day|week|month|year\n --jitter <dur> max random nudge per fire, e.g. \"5m\"\n -i, --immediate fire once immediately on start\n\nExamples:\n habit start --every \"10s ~ 2s\" -- echo \"stretch\"\n habit start --name sync --times 3 --per hour --jitter 5m -- npm run sync\n habit list\n";
|
|
31
|
+
/** CLI entry. Returns the intended exit code (daemon never returns). */
|
|
32
|
+
declare function main(argv: string[]): Promise<number>;
|
|
33
|
+
|
|
34
|
+
export { HELP, VERSION, main, parseArgs, toOptions };
|
|
35
|
+
export type { CliArgs, ParseResult };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Period, Schedule } from '../core/index.mjs';
|
|
2
|
+
|
|
3
|
+
declare const VERSION = "0.2.0";
|
|
4
|
+
interface CliArgs {
|
|
5
|
+
name?: string;
|
|
6
|
+
every?: string;
|
|
7
|
+
times?: number;
|
|
8
|
+
per?: Period;
|
|
9
|
+
jitter?: string;
|
|
10
|
+
immediate: boolean;
|
|
11
|
+
max?: number;
|
|
12
|
+
help: boolean;
|
|
13
|
+
version: boolean;
|
|
14
|
+
command: string[];
|
|
15
|
+
}
|
|
16
|
+
interface ParseResult {
|
|
17
|
+
args?: CliArgs;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Parse schedule flags + command (the bit shared by `run`/`start`/`update`). */
|
|
21
|
+
declare function parseArgs(argv: string[]): ParseResult;
|
|
22
|
+
/** Build core {@link Schedule}-based options for `run` (foreground). */
|
|
23
|
+
declare function toOptions(args: CliArgs): {
|
|
24
|
+
options?: Schedule & {
|
|
25
|
+
immediate?: boolean;
|
|
26
|
+
autoStart?: boolean;
|
|
27
|
+
};
|
|
28
|
+
error?: string;
|
|
29
|
+
};
|
|
30
|
+
declare const HELP = "habit v0.2.0 \u2014 randomized recurring schedules (\"habits\", not cronjobs)\n\nUsage:\n habit start [--name <n>] <schedule> -- <command...> create + run in the background\n habit run <schedule> -- <command...> run attached (Ctrl-C to stop)\n habit list list habits and what they run\n habit stop <id|name|all> pause\n habit start <id|name> resume a paused habit\n habit restart <id|name> restart\n habit update <id|name> [--every \u2026 | --name \u2026 | -- <command...>]\n habit delete <id|name|all> remove (alias: rm)\n habit logs <id|name> [-n <lines>] show recent output\n habit kill stop the background daemon\n\nSchedule:\n --every <dur> interval, e.g. \"2h\", \"10s ~ 2s\", \"1h30m\"\n --times <n> --per <period> N times per minute|hour|day|week|month|year\n --jitter <dur> max random nudge per fire, e.g. \"5m\"\n -i, --immediate fire once immediately on start\n\nExamples:\n habit start --every \"10s ~ 2s\" -- echo \"stretch\"\n habit start --name sync --times 3 --per hour --jitter 5m -- npm run sync\n habit list\n";
|
|
31
|
+
/** CLI entry. Returns the intended exit code (daemon never returns). */
|
|
32
|
+
declare function main(argv: string[]): Promise<number>;
|
|
33
|
+
|
|
34
|
+
export { HELP, VERSION, main, parseArgs, toOptions };
|
|
35
|
+
export type { CliArgs, ParseResult };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Period, Schedule } from '../core/index.js';
|
|
2
|
+
|
|
3
|
+
declare const VERSION = "0.2.0";
|
|
4
|
+
interface CliArgs {
|
|
5
|
+
name?: string;
|
|
6
|
+
every?: string;
|
|
7
|
+
times?: number;
|
|
8
|
+
per?: Period;
|
|
9
|
+
jitter?: string;
|
|
10
|
+
immediate: boolean;
|
|
11
|
+
max?: number;
|
|
12
|
+
help: boolean;
|
|
13
|
+
version: boolean;
|
|
14
|
+
command: string[];
|
|
15
|
+
}
|
|
16
|
+
interface ParseResult {
|
|
17
|
+
args?: CliArgs;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Parse schedule flags + command (the bit shared by `run`/`start`/`update`). */
|
|
21
|
+
declare function parseArgs(argv: string[]): ParseResult;
|
|
22
|
+
/** Build core {@link Schedule}-based options for `run` (foreground). */
|
|
23
|
+
declare function toOptions(args: CliArgs): {
|
|
24
|
+
options?: Schedule & {
|
|
25
|
+
immediate?: boolean;
|
|
26
|
+
autoStart?: boolean;
|
|
27
|
+
};
|
|
28
|
+
error?: string;
|
|
29
|
+
};
|
|
30
|
+
declare const HELP = "habit v0.2.0 \u2014 randomized recurring schedules (\"habits\", not cronjobs)\n\nUsage:\n habit start [--name <n>] <schedule> -- <command...> create + run in the background\n habit run <schedule> -- <command...> run attached (Ctrl-C to stop)\n habit list list habits and what they run\n habit stop <id|name|all> pause\n habit start <id|name> resume a paused habit\n habit restart <id|name> restart\n habit update <id|name> [--every \u2026 | --name \u2026 | -- <command...>]\n habit delete <id|name|all> remove (alias: rm)\n habit logs <id|name> [-n <lines>] show recent output\n habit kill stop the background daemon\n\nSchedule:\n --every <dur> interval, e.g. \"2h\", \"10s ~ 2s\", \"1h30m\"\n --times <n> --per <period> N times per minute|hour|day|week|month|year\n --jitter <dur> max random nudge per fire, e.g. \"5m\"\n -i, --immediate fire once immediately on start\n\nExamples:\n habit start --every \"10s ~ 2s\" -- echo \"stretch\"\n habit start --name sync --times 3 --per hour --jitter 5m -- npm run sync\n habit list\n";
|
|
31
|
+
/** CLI entry. Returns the intended exit code (daemon never returns). */
|
|
32
|
+
declare function main(argv: string[]): Promise<number>;
|
|
33
|
+
|
|
34
|
+
export { HELP, VERSION, main, parseArgs, toOptions };
|
|
35
|
+
export type { CliArgs, ParseResult };
|