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.
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import{spawn as I}from"node:child_process";import{readFileSync as H,mkdirSync as P,writeFileSync as V,existsSync as K,appendFileSync as Q}from"node:fs";import u from"node:process";import{createHabit as A}from"../core/index.mjs";import{join as b,basename as X,dirname as Y}from"node:path";import{homedir as Z}from"node:os";function g(){return u.env.HABIT_HOME??b(Z(),".habit")}const F=()=>b(g(),"habits.json"),G=()=>b(g(),"state.json"),$=()=>b(g(),"daemon.json"),L=e=>b(g(),"logs",`${e}.log`);function E(e,t){try{return JSON.parse(H(e,"utf8"))}catch{return t}}function y(e,t){P(g(),{recursive:!0}),V(e,`${JSON.stringify(t,null,2)}
3
+ `)}function h(){return E(F(),[])}function O(e){y(F(),e)}function p(e,t){return e.find(n=>n.id===t)??e.find(n=>n.name===t)}function ee(e){const t=e.reduce((n,r)=>Math.max(n,Number(r.id)||0),0);return String(t+1)}function te(e){const t=e.find(n=>/\.(?:[cm]?js|ts|sh|py|rb)$/.test(n))??e[0]??"habit";return X(t).replace(/\.[^.]+$/,"")}function ne(e){const t=h(),n={id:ee(t),name:e.name??te(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),O(t),n}function j(e,t){const n=h(),r=p(n,e);if(r)return Object.assign(r,t),O(n),r}function W(e){const t=h(),n=p(t,e);if(!n)return;O(t.filter(i=>i!==n));const r=S();return delete r[n.id],_(r),n}function S(){return E(G(),{})}function _(e){y(G(),e)}const re={counter:0,lastRun:null,lastExit:null,nextRun:null,startedAt:null};function f(e,t){const n=S();n[e]={...re,...n[e],...t},_(n)}function R(){return E($(),null)}function ie(e){y($(),e)}function ae(){K($())&&y($(),null)}function D(){const e=R();if(e?.pid==null)return!1;try{return u.kill(e.pid,0),!0}catch{return!1}}function oe(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 se(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 ue(e){if(e==null)return"\u2014";const t=Date.now()-new Date(e).getTime();return t<0?`in ${N(-t)}`:`${N(t)} ago`}function N(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 ce(e,t){if(t==="stopped"||e==null)return"\u2014";const n=new Date(e).getTime()-Date.now();return n<=0?"now":`in ${N(n)}`}function me(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(o=>{const a=t[o.id];return[o.id,o.name,o.status,se(o),o.command.join(" "),String(a?.counter??0),ce(a?.nextRun??null,o.status),ue(a?.lastRun??null)]}),i=n.map((o,a)=>Math.max(o.length,...r.map(l=>l[a].length))),s=o=>o.map((a,l)=>a.padEnd(i[l])).join(" ").trimEnd();return[s(n),s(i.map(o=>"\u2500".repeat(o))),...r.map(s)].join(`
4
+ `)}function v(e,t){const n=L(e);P(Y(n),{recursive:!0}),Q(n,t)}async function le(e){return new Promise(t=>{const n=new Date;v(e.id,`
5
+ [${n.toISOString()}] $ ${e.command.join(" ")}
6
+ `);let r;try{const[i,...s]=e.command;r=I(i,s,{stdio:["ignore","pipe","pipe"],shell:!1})}catch(i){const s=i instanceof Error?i.message:String(i);v(e.id,`[habit] spawn failed: ${s}
7
+ `),f(e.id,{lastRun:n.toISOString(),lastExit:null}),t();return}r.stdout?.on("data",i=>v(e.id,i)),r.stderr?.on("data",i=>v(e.id,i)),r.on("error",i=>{v(e.id,`[habit] ${i.message}
8
+ `),f(e.id,{lastRun:n.toISOString(),lastExit:null}),t()}),r.on("close",i=>{f(e.id,{lastRun:n.toISOString(),lastExit:i}),t()})})}function de(){ie({pid:u.pid,startedAt:new Date().toISOString()});const e=new Map,t=a=>{const l=A(async()=>le(a),oe(a));return l.subscribe(()=>{f(a.id,{counter:l.counter,nextRun:l.nextRun?.toISOString()??null})}),f(a.id,{startedAt:new Date().toISOString()}),l.start(a.immediate??!1),{ctrl:l,rev:a.rev}},n=a=>{const l=e.get(a);l&&(l.ctrl.destroy(),e.delete(a),f(a,{nextRun:null,startedAt:null}))},r=()=>{const a=h(),l=new Set(a.map(d=>d.id));for(const d of[...e.keys()])l.has(d)||n(d);for(const d of a){const k=e.get(d.id);if(d.status!=="running"){k&&n(d.id);continue}k?k.rev!==d.rev&&(n(d.id),e.set(d.id,t(d))):e.set(d.id,t(d))}};r();const i=setInterval(r,1e3),s=()=>{clearInterval(i);for(const a of[...e.keys()])n(a);ae(),u.exit(0)};u.on("SIGTERM",s),u.on("SIGINT",s);const o=S();for(const a of Object.keys(o))e.has(a)||f(a,{nextRun:null,startedAt:null})}const M="0.2.0",C=["minute","hour","day","week","month","year"],he=C;function fe(e){return e!=null&&he.includes(e)}function w(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(!fe(i))return{error:`--per expects one of ${C.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 q(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 pe(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 J=`habit v${M} \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 m(e){u.stdout.write(`${e}
33
+ `)}function c(e){u.stderr.write(`[habit] ${e}
34
+ `)}function x(){if(D())return;const e=u.argv[1];I(u.execPath,[e,"__daemon"],{detached:!0,stdio:"ignore"}).unref()}function T(e){return`${e.id} (${e.name}) \u2192 ${e.command.join(" ")}`}function be(e){if(e.length===1&&!e[0].startsWith("-")){const o=p(h(),e[0]);if(o)return j(o.id,{status:"running",rev:o.rev+1}),x(),m(`resumed ${T(o)}`),0}const{args:t,error:n}=w(e);if(n!=null||!t)return c(n??"parse error"),1;const{habit:r,error:i}=pe(t);if(i!=null||!r)return c(i??"invalid habit"),1;const s=ne(r);return x(),m(`started ${T(s)}`),m(` ${t.every!=null?`every ${t.every}`:`${t.times}\xD7/${t.per}`}${t.jitter!=null?` ~ ${t.jitter}`:""}`),0}function z(e,t){const n=h();let r;if(e==="all")r=n;else{const i=p(n,e);r=i?[i]:[]}if(r.length===0)return c(`no habit matching "${e}"`),1;for(const i of r)t(i);return 0}function ge(e){return e[0]?z(e[0],t=>{j(t.id,{status:"stopped"}),m(`stopped ${t.id} (${t.name})`)}):(c("usage: habit stop <id|name|all>"),1)}function ve(e){if(!e[0])return c("usage: habit restart <id|name|all>"),1;const t=z(e[0],n=>{j(n.id,{status:"running",rev:n.rev+1}),m(`restarted ${n.id} (${n.name})`)});return t===0&&x(),t}function $e(e){if(!e[0])return c("usage: habit delete <id|name|all>"),1;if(e[0]==="all"){const n=h();if(n.length===0)return c("no habits to delete"),1;for(const r of n)W(r.id);return m(`deleted ${n.length} habit(s)`),0}const t=W(e[0]);return t?(m(`deleted ${t.id} (${t.name})`),0):(c(`no habit matching "${e[0]}"`),1)}function ye(e){const t=e[0];if(!t||t.startsWith("-"))return c("usage: habit update <id|name> [--every \u2026 | --name \u2026 | -- <command...>]"),1;const n=p(h(),t);if(!n)return c(`no habit matching "${t}"`),1;const{args:r,error:i}=w(e.slice(1));if(i!=null||!r)return c(i??"parse error"),1;const s={rev:n.rev+1};r.name!=null&&(s.name=r.name),r.every!=null&&(s.every=r.every,s.times=void 0,s.per=void 0),r.times!=null&&(s.times=r.times),r.per!=null&&(s.per=r.per),r.jitter!=null&&(s.jitter=r.jitter),r.immediate&&(s.immediate=!0),r.command.length>0&&(s.command=r.command);const o=j(n.id,s);return o?(x(),m(`updated ${T(o)}`),0):(c(`no habit matching "${t}"`),1)}function je(){m(me(h(),S()));const e=R();return m(""),m(D()&&e?`daemon: running (pid ${e.pid})`:"daemon: not running"),0}function Se(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 c("usage: habit logs <id|name> [-n <lines>]"),1;const r=p(h(),t);if(!r)return c(`no habit matching "${t}"`),1;try{const i=H(L(r.id),"utf8").trimEnd().split(`
35
+ `).slice(-n).join(`
36
+ `);m(i||"(no output yet)")}catch{m("(no output yet)")}return 0}function we(){const e=R();if(e?.pid==null||!D())return m("daemon: not running"),0;try{u.kill(e.pid,"SIGTERM"),m(`stopped daemon (pid ${e.pid})`)}catch(t){return c(`could not stop daemon: ${t instanceof Error?t.message:String(t)}`),1}return 0}async function B(e){const{args:t,error:n}=w(e);if(n!=null||!t)return c(n??"parse error"),1;if(t.command.length===0)return c('no command given. Pass one after "--".'),1;const{options:r,error:i}=q(t);return i!=null||!r?(c(i??"invalid schedule"),1):(await new Promise(s=>{const o=A(async()=>{const l=new Date().toLocaleTimeString();return m(`[habit] ${l} \u2192 ${t.command.join(" ")}`),xe(t.command)},r);o.start(t.immediate),o.nextRun&&m(`[habit] first run at ${o.nextRun.toLocaleTimeString()}`);const a=()=>{o.stop(),m(`
37
+ [habit] stopped`),s()};u.on("SIGINT",a),u.on("SIGTERM",a)}),0)}async function xe(e){return new Promise(t=>{const[n,...r]=e,i=I(n,r,{stdio:"inherit",shell:!1});i.on("error",s=>c(`command failed: ${s.message}`)),i.on("close",()=>t())})}async function U(e){const[t,...n]=e;if(t==null||t==="-h"||t==="--help")return u.stdout.write(J),0;if(t==="-v"||t==="--version")return m(M),0;switch(t){case"__daemon":return de(),new Promise(()=>{});case"run":return B(n);case"start":return be(n);case"stop":return ge(n);case"restart":return ve(n);case"update":return ye(n);case"delete":case"rm":return $e(n);case"list":case"ls":return je();case"logs":return Se(n);case"kill":return we();default:return t.startsWith("-")?B(e):(c(`unknown command "${t}". Try: habit --help`),1)}}const ke=typeof u<"u"&&u.argv[1]!=null&&/habit|cli[\\/]index/.test(u.argv[1]);ke&&(u.stdout.on("error",e=>{e.code==="EPIPE"&&u.exit(0)}),U(u.argv.slice(2)).then(e=>{u.exitCode=e}));export{J as HELP,M as VERSION,U as main,w as parseArgs,q as toOptions};
@@ -0,0 +1 @@
1
+ "use strict";const A={minute:6e4,hour:36e5,day:864e5,week:6048e5,month:2629746e3,year:315576e5},g=2147483647,E={ms:1,sec:1e3,min:6e4,mo:2629746e3,hr:36e5,w:6048e5,s:1e3,m:6e4,h:36e5,d:864e5,y:315576e5},h=/(\d+(?:\.\d+)?)\s*(ms|sec|min|mo|hr|[smhdwy])/g,O=/~|\+\/?-|±/;function dur(t){if(typeof t=="number")return t;if(typeof t!="string")return 0;let n=0,r;for(h.lastIndex=0,r=h.exec(t);r!=null;)n+=Number.parseFloat(r[1])*E[r[2]],r=h.exec(t);return n}function resolveJitter(t){if(t==null)return null;if(typeof t=="number"||typeof t=="string"){const n=dur(t);return n>0?{min:0,max:n}:null}return Array.isArray(t)?{min:dur(t[0]),max:dur(t[1])}:{min:dur(t.min??0),max:dur(t.max)}}function normalize(t){let n=0,r=resolveJitter(t.jitter);if("every"in t&&t.every!=null)if(typeof t.every=="string"&&O.test(t.every)){const[i,l]=t.every.split(O);n=dur(i.trim()),r||(r=resolveJitter(l.trim()))}else n=dur(t.every);else"times"in t&&t.times&&t.per&&A[t.per]&&(n=A[t.per]/t.times);return n>0&&Number.isFinite(n)?{intervalMs:n,jitter:r}:null}function longTimeout(t,n){let r,i=Math.max(0,n),l=!1;const c=()=>{l||(i<=g?r=setTimeout(t,i):(i-=g,r=setTimeout(c,g)))};return c(),()=>{l=!0,clearTimeout(r)}}function k(t){return("habits"in t?t.habits:[t]).map(normalize).filter(n=>n!=null).map(n=>({...n,anchor:0,count:0,nextTs:null,cancel:null}))}let P=0;const p=new Map,D=new Set;function H(){for(const t of D)t()}function listHabits(){return[...p.values()]}function getHabit(t){return p.get(t)}function subscribeHabits(t){return D.add(t),()=>{D.delete(t)}}function clearHabits(){for(const t of[...p.values()])t.destroy()}function createHabit(t,n){const r=n,{immediate:i=!1,autoStart:l=!0,random:c=Math.random}=r,d=r.id??`h${++P}`;let T=r.name,m=k(r),b=0,s=!1,w=null;const y=new Set,a=()=>{for(const e of y)e()},J=(e,o)=>e+c()*(o-e),v=()=>{let e=null;for(const o of m)o.nextTs!=null&&(e==null||o.nextTs<e)&&(e=o.nextTs);w=e==null?null:new Date(e)},M=()=>{b++,a();try{const e=t();e&&typeof e.then=="function"&&e.catch(()=>{})}catch{}},z=e=>{if(!e.jitter)return 0;let o=J(e.jitter.min,e.jitter.max);const u=e.intervalMs*.49;return o>u&&(o=u),(c()<.5?-1:1)*o},j=e=>{e.count++;const o=e.anchor+e.count*e.intervalMs+z(e);e.nextTs=o,v(),a(),e.cancel=longTimeout(()=>{M(),s&&j(e)},o-Date.now())},f=(e=!1)=>{if(s)return;const o=Date.now();for(const u of m)u.anchor=o,u.count=0;s=!0,a(),e&&M();for(const u of m)j(u)},x=()=>{for(const e of m)e.cancel?.(),e.cancel=null,e.nextTs=null;s=!1,v(),a()},S={id:d,get name(){return T},get counter(){return b},get isActive(){return s},get nextRun(){return w},start:f,stop:x,pause:()=>{s&&x()},resume:()=>{s||f(!1)},reset:()=>{b=0,a(),s&&(x(),f(!1))},update:e=>{const o=s;s&&x(),e.name!==void 0&&(T=e.name),m=k(e),o?f(!1):(v(),a()),H()},destroy:()=>{x(),p.delete(d),H()},subscribe:e=>(y.add(e),()=>{y.delete(e)})};return p.set(d,S),H(),l&&f(i),S}exports.clearHabits=clearHabits,exports.createHabit=createHabit,exports.dur=dur,exports.getHabit=getHabit,exports.listHabits=listHabits,exports.longTimeout=longTimeout,exports.normalize=normalize,exports.resolveJitter=resolveJitter,exports.subscribeHabits=subscribeHabits;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * habicron core — the framework-agnostic randomized recurring scheduler.
3
+ *
4
+ * A "habit" is `{ intervalMs, jitter }`. The engine fires a callback on the
5
+ * union of all habits, each habit reschedules itself, and every next fire is
6
+ * computed against a fixed grid (`anchor + count * interval`) so the long-run
7
+ * rate stays exact — accurate by default, optionally jittered with no drift.
8
+ *
9
+ * This module has no framework or platform dependencies. The Vue, React, Node
10
+ * and CLI entry points are thin adapters over {@link createHabit}.
11
+ */
12
+ /** Milliseconds, or a duration string: `'2h'`, `'20s'`, `'1h30m'`, `'500ms'`, `'3d'`. */
13
+ type Duration = number | string;
14
+ /**
15
+ * Jitter magnitude (sign is always random — fires land earlier OR later).
16
+ * - `Duration` → max magnitude, min 0 e.g. `'5m'`
17
+ * - `[min, max]` → bounded magnitude e.g. `['3s', '5s']`
18
+ * - `{ min?, max }` → bounded magnitude, object form
19
+ */
20
+ type Jitter = Duration | [min: Duration, max: Duration] | {
21
+ min?: Duration;
22
+ max: Duration;
23
+ };
24
+ type Period = 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
25
+ /**
26
+ * One recurring habit. Use `every` (interval between fires) or `times`/`per`
27
+ * ("N times per period") — never both. Jitter lives inline.
28
+ *
29
+ * `every` also accepts a packed form: `'2h ~ 5m'` (cadence ~ max jitter).
30
+ */
31
+ type Schedule = {
32
+ every: Duration;
33
+ jitter?: Jitter;
34
+ times?: never;
35
+ per?: never;
36
+ } | {
37
+ times: number;
38
+ per: Period;
39
+ jitter?: Jitter;
40
+ every?: never;
41
+ };
42
+ interface ControlFlags {
43
+ /** Stable identifier for the registry. Auto-generated when omitted. */
44
+ id?: string;
45
+ /** Human-friendly label shown by management tooling (`listHabits`, the CLI). */
46
+ name?: string;
47
+ /** Fire once immediately on start (counts toward `counter`). */
48
+ immediate?: boolean;
49
+ /**
50
+ * Start the timers as soon as the controller is created. Default `true`.
51
+ * Adapters set this to `false` to stay inert during SSR.
52
+ */
53
+ autoStart?: boolean;
54
+ /**
55
+ * Random source in `[0, 1)`. Defaults to `Math.random`. Injecting a seeded
56
+ * RNG makes jitter deterministic (used by the test-suite and the docs demo).
57
+ */
58
+ random?: () => number;
59
+ }
60
+ /** A single inline schedule, or an explicit list of overlapping habits. */
61
+ type HabitOptions = ControlFlags & (Schedule | {
62
+ habits: Schedule[];
63
+ });
64
+ /** The reactive surface every adapter maps onto its own primitives. */
65
+ interface HabitController {
66
+ /** Stable identifier in the registry. */
67
+ readonly id: string;
68
+ /** Human-friendly label, or `undefined`. */
69
+ readonly name: string | undefined;
70
+ /** Total number of times the callback has fired. */
71
+ readonly counter: number;
72
+ /** Whether timers are currently running. */
73
+ readonly isActive: boolean;
74
+ /** Earliest upcoming fire across all habits, or `null` when stopped. */
75
+ readonly nextRun: Date | null;
76
+ /** (Re)start every habit from now. Pass `true` to fire once immediately. */
77
+ start: (immediate?: boolean) => void;
78
+ /** Cancel all timers and clear `nextRun`. `counter` is preserved. */
79
+ stop: () => void;
80
+ /** Pause if active (alias of `stop` that no-ops when already stopped). */
81
+ pause: () => void;
82
+ /** Resume if stopped (restarts from now without an immediate fire). */
83
+ resume: () => void;
84
+ /** Reset `counter` to 0 and, if active, restart all habits from now. */
85
+ reset: () => void;
86
+ /** Replace the schedule (and optionally the `name`); reschedules in place. */
87
+ update: (options: HabitOptions) => void;
88
+ /** Stop all timers and remove this habit from the registry. */
89
+ destroy: () => void;
90
+ /** Subscribe to state changes; returns an unsubscribe function. */
91
+ subscribe: (listener: () => void) => () => void;
92
+ }
93
+ /** A plain, serialisable snapshot of a habit's state (for listing UIs). */
94
+ interface HabitSummary {
95
+ id: string;
96
+ name: string | undefined;
97
+ isActive: boolean;
98
+ counter: number;
99
+ nextRun: Date | null;
100
+ }
101
+ /** Parse a duration: number (ms) or string like `'2h'`, `'1h30m'`, `'500ms'`. */
102
+ declare function dur(v: Duration): number;
103
+ interface JitterRange {
104
+ min: number;
105
+ max: number;
106
+ }
107
+ /** Normalise a jitter spec into a `{ min, max }` magnitude range in ms, or null. */
108
+ declare function resolveJitter(j?: Jitter): JitterRange | null;
109
+ interface Spec {
110
+ intervalMs: number;
111
+ jitter: JitterRange | null;
112
+ }
113
+ /** Reduce one schedule spec to `{ intervalMs, jitter }`, or null when invalid. */
114
+ declare function normalize(s: Schedule): Spec | null;
115
+ /** `setTimeout` that survives delays beyond the 32-bit ceiling. Returns a cancel. */
116
+ declare function longTimeout(fn: () => void, delay: number): () => void;
117
+ /** Every registered habit (active or paused), in creation order. */
118
+ declare function listHabits(): HabitController[];
119
+ /** Look up a registered habit by its id. */
120
+ declare function getHabit(id: string): HabitController | undefined;
121
+ /** Subscribe to registry changes (a habit added or removed). Returns unsubscribe. */
122
+ declare function subscribeHabits(listener: () => void): () => void;
123
+ /** Destroy every registered habit (stops timers and empties the registry). */
124
+ declare function clearHabits(): void;
125
+ /**
126
+ * Create a framework-agnostic habit scheduler. The returned controller is
127
+ * registered so it can be found via {@link listHabits} / {@link getHabit}, and
128
+ * removed with `controller.destroy()`.
129
+ *
130
+ * @example
131
+ * const job = createHabit(() => console.log('tick'), { every: '2h ~ 5m' })
132
+ * job.pause()
133
+ */
134
+ declare function createHabit(callback: () => void | Promise<void>, options: HabitOptions): HabitController;
135
+
136
+ export { clearHabits, createHabit, dur, getHabit, listHabits, longTimeout, normalize, resolveJitter, subscribeHabits };
137
+ export type { ControlFlags, Duration, HabitController, HabitOptions, HabitSummary, Jitter, Period, Schedule };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * habicron core — the framework-agnostic randomized recurring scheduler.
3
+ *
4
+ * A "habit" is `{ intervalMs, jitter }`. The engine fires a callback on the
5
+ * union of all habits, each habit reschedules itself, and every next fire is
6
+ * computed against a fixed grid (`anchor + count * interval`) so the long-run
7
+ * rate stays exact — accurate by default, optionally jittered with no drift.
8
+ *
9
+ * This module has no framework or platform dependencies. The Vue, React, Node
10
+ * and CLI entry points are thin adapters over {@link createHabit}.
11
+ */
12
+ /** Milliseconds, or a duration string: `'2h'`, `'20s'`, `'1h30m'`, `'500ms'`, `'3d'`. */
13
+ type Duration = number | string;
14
+ /**
15
+ * Jitter magnitude (sign is always random — fires land earlier OR later).
16
+ * - `Duration` → max magnitude, min 0 e.g. `'5m'`
17
+ * - `[min, max]` → bounded magnitude e.g. `['3s', '5s']`
18
+ * - `{ min?, max }` → bounded magnitude, object form
19
+ */
20
+ type Jitter = Duration | [min: Duration, max: Duration] | {
21
+ min?: Duration;
22
+ max: Duration;
23
+ };
24
+ type Period = 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
25
+ /**
26
+ * One recurring habit. Use `every` (interval between fires) or `times`/`per`
27
+ * ("N times per period") — never both. Jitter lives inline.
28
+ *
29
+ * `every` also accepts a packed form: `'2h ~ 5m'` (cadence ~ max jitter).
30
+ */
31
+ type Schedule = {
32
+ every: Duration;
33
+ jitter?: Jitter;
34
+ times?: never;
35
+ per?: never;
36
+ } | {
37
+ times: number;
38
+ per: Period;
39
+ jitter?: Jitter;
40
+ every?: never;
41
+ };
42
+ interface ControlFlags {
43
+ /** Stable identifier for the registry. Auto-generated when omitted. */
44
+ id?: string;
45
+ /** Human-friendly label shown by management tooling (`listHabits`, the CLI). */
46
+ name?: string;
47
+ /** Fire once immediately on start (counts toward `counter`). */
48
+ immediate?: boolean;
49
+ /**
50
+ * Start the timers as soon as the controller is created. Default `true`.
51
+ * Adapters set this to `false` to stay inert during SSR.
52
+ */
53
+ autoStart?: boolean;
54
+ /**
55
+ * Random source in `[0, 1)`. Defaults to `Math.random`. Injecting a seeded
56
+ * RNG makes jitter deterministic (used by the test-suite and the docs demo).
57
+ */
58
+ random?: () => number;
59
+ }
60
+ /** A single inline schedule, or an explicit list of overlapping habits. */
61
+ type HabitOptions = ControlFlags & (Schedule | {
62
+ habits: Schedule[];
63
+ });
64
+ /** The reactive surface every adapter maps onto its own primitives. */
65
+ interface HabitController {
66
+ /** Stable identifier in the registry. */
67
+ readonly id: string;
68
+ /** Human-friendly label, or `undefined`. */
69
+ readonly name: string | undefined;
70
+ /** Total number of times the callback has fired. */
71
+ readonly counter: number;
72
+ /** Whether timers are currently running. */
73
+ readonly isActive: boolean;
74
+ /** Earliest upcoming fire across all habits, or `null` when stopped. */
75
+ readonly nextRun: Date | null;
76
+ /** (Re)start every habit from now. Pass `true` to fire once immediately. */
77
+ start: (immediate?: boolean) => void;
78
+ /** Cancel all timers and clear `nextRun`. `counter` is preserved. */
79
+ stop: () => void;
80
+ /** Pause if active (alias of `stop` that no-ops when already stopped). */
81
+ pause: () => void;
82
+ /** Resume if stopped (restarts from now without an immediate fire). */
83
+ resume: () => void;
84
+ /** Reset `counter` to 0 and, if active, restart all habits from now. */
85
+ reset: () => void;
86
+ /** Replace the schedule (and optionally the `name`); reschedules in place. */
87
+ update: (options: HabitOptions) => void;
88
+ /** Stop all timers and remove this habit from the registry. */
89
+ destroy: () => void;
90
+ /** Subscribe to state changes; returns an unsubscribe function. */
91
+ subscribe: (listener: () => void) => () => void;
92
+ }
93
+ /** A plain, serialisable snapshot of a habit's state (for listing UIs). */
94
+ interface HabitSummary {
95
+ id: string;
96
+ name: string | undefined;
97
+ isActive: boolean;
98
+ counter: number;
99
+ nextRun: Date | null;
100
+ }
101
+ /** Parse a duration: number (ms) or string like `'2h'`, `'1h30m'`, `'500ms'`. */
102
+ declare function dur(v: Duration): number;
103
+ interface JitterRange {
104
+ min: number;
105
+ max: number;
106
+ }
107
+ /** Normalise a jitter spec into a `{ min, max }` magnitude range in ms, or null. */
108
+ declare function resolveJitter(j?: Jitter): JitterRange | null;
109
+ interface Spec {
110
+ intervalMs: number;
111
+ jitter: JitterRange | null;
112
+ }
113
+ /** Reduce one schedule spec to `{ intervalMs, jitter }`, or null when invalid. */
114
+ declare function normalize(s: Schedule): Spec | null;
115
+ /** `setTimeout` that survives delays beyond the 32-bit ceiling. Returns a cancel. */
116
+ declare function longTimeout(fn: () => void, delay: number): () => void;
117
+ /** Every registered habit (active or paused), in creation order. */
118
+ declare function listHabits(): HabitController[];
119
+ /** Look up a registered habit by its id. */
120
+ declare function getHabit(id: string): HabitController | undefined;
121
+ /** Subscribe to registry changes (a habit added or removed). Returns unsubscribe. */
122
+ declare function subscribeHabits(listener: () => void): () => void;
123
+ /** Destroy every registered habit (stops timers and empties the registry). */
124
+ declare function clearHabits(): void;
125
+ /**
126
+ * Create a framework-agnostic habit scheduler. The returned controller is
127
+ * registered so it can be found via {@link listHabits} / {@link getHabit}, and
128
+ * removed with `controller.destroy()`.
129
+ *
130
+ * @example
131
+ * const job = createHabit(() => console.log('tick'), { every: '2h ~ 5m' })
132
+ * job.pause()
133
+ */
134
+ declare function createHabit(callback: () => void | Promise<void>, options: HabitOptions): HabitController;
135
+
136
+ export { clearHabits, createHabit, dur, getHabit, listHabits, longTimeout, normalize, resolveJitter, subscribeHabits };
137
+ export type { ControlFlags, Duration, HabitController, HabitOptions, HabitSummary, Jitter, Period, Schedule };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * habicron core — the framework-agnostic randomized recurring scheduler.
3
+ *
4
+ * A "habit" is `{ intervalMs, jitter }`. The engine fires a callback on the
5
+ * union of all habits, each habit reschedules itself, and every next fire is
6
+ * computed against a fixed grid (`anchor + count * interval`) so the long-run
7
+ * rate stays exact — accurate by default, optionally jittered with no drift.
8
+ *
9
+ * This module has no framework or platform dependencies. The Vue, React, Node
10
+ * and CLI entry points are thin adapters over {@link createHabit}.
11
+ */
12
+ /** Milliseconds, or a duration string: `'2h'`, `'20s'`, `'1h30m'`, `'500ms'`, `'3d'`. */
13
+ type Duration = number | string;
14
+ /**
15
+ * Jitter magnitude (sign is always random — fires land earlier OR later).
16
+ * - `Duration` → max magnitude, min 0 e.g. `'5m'`
17
+ * - `[min, max]` → bounded magnitude e.g. `['3s', '5s']`
18
+ * - `{ min?, max }` → bounded magnitude, object form
19
+ */
20
+ type Jitter = Duration | [min: Duration, max: Duration] | {
21
+ min?: Duration;
22
+ max: Duration;
23
+ };
24
+ type Period = 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
25
+ /**
26
+ * One recurring habit. Use `every` (interval between fires) or `times`/`per`
27
+ * ("N times per period") — never both. Jitter lives inline.
28
+ *
29
+ * `every` also accepts a packed form: `'2h ~ 5m'` (cadence ~ max jitter).
30
+ */
31
+ type Schedule = {
32
+ every: Duration;
33
+ jitter?: Jitter;
34
+ times?: never;
35
+ per?: never;
36
+ } | {
37
+ times: number;
38
+ per: Period;
39
+ jitter?: Jitter;
40
+ every?: never;
41
+ };
42
+ interface ControlFlags {
43
+ /** Stable identifier for the registry. Auto-generated when omitted. */
44
+ id?: string;
45
+ /** Human-friendly label shown by management tooling (`listHabits`, the CLI). */
46
+ name?: string;
47
+ /** Fire once immediately on start (counts toward `counter`). */
48
+ immediate?: boolean;
49
+ /**
50
+ * Start the timers as soon as the controller is created. Default `true`.
51
+ * Adapters set this to `false` to stay inert during SSR.
52
+ */
53
+ autoStart?: boolean;
54
+ /**
55
+ * Random source in `[0, 1)`. Defaults to `Math.random`. Injecting a seeded
56
+ * RNG makes jitter deterministic (used by the test-suite and the docs demo).
57
+ */
58
+ random?: () => number;
59
+ }
60
+ /** A single inline schedule, or an explicit list of overlapping habits. */
61
+ type HabitOptions = ControlFlags & (Schedule | {
62
+ habits: Schedule[];
63
+ });
64
+ /** The reactive surface every adapter maps onto its own primitives. */
65
+ interface HabitController {
66
+ /** Stable identifier in the registry. */
67
+ readonly id: string;
68
+ /** Human-friendly label, or `undefined`. */
69
+ readonly name: string | undefined;
70
+ /** Total number of times the callback has fired. */
71
+ readonly counter: number;
72
+ /** Whether timers are currently running. */
73
+ readonly isActive: boolean;
74
+ /** Earliest upcoming fire across all habits, or `null` when stopped. */
75
+ readonly nextRun: Date | null;
76
+ /** (Re)start every habit from now. Pass `true` to fire once immediately. */
77
+ start: (immediate?: boolean) => void;
78
+ /** Cancel all timers and clear `nextRun`. `counter` is preserved. */
79
+ stop: () => void;
80
+ /** Pause if active (alias of `stop` that no-ops when already stopped). */
81
+ pause: () => void;
82
+ /** Resume if stopped (restarts from now without an immediate fire). */
83
+ resume: () => void;
84
+ /** Reset `counter` to 0 and, if active, restart all habits from now. */
85
+ reset: () => void;
86
+ /** Replace the schedule (and optionally the `name`); reschedules in place. */
87
+ update: (options: HabitOptions) => void;
88
+ /** Stop all timers and remove this habit from the registry. */
89
+ destroy: () => void;
90
+ /** Subscribe to state changes; returns an unsubscribe function. */
91
+ subscribe: (listener: () => void) => () => void;
92
+ }
93
+ /** A plain, serialisable snapshot of a habit's state (for listing UIs). */
94
+ interface HabitSummary {
95
+ id: string;
96
+ name: string | undefined;
97
+ isActive: boolean;
98
+ counter: number;
99
+ nextRun: Date | null;
100
+ }
101
+ /** Parse a duration: number (ms) or string like `'2h'`, `'1h30m'`, `'500ms'`. */
102
+ declare function dur(v: Duration): number;
103
+ interface JitterRange {
104
+ min: number;
105
+ max: number;
106
+ }
107
+ /** Normalise a jitter spec into a `{ min, max }` magnitude range in ms, or null. */
108
+ declare function resolveJitter(j?: Jitter): JitterRange | null;
109
+ interface Spec {
110
+ intervalMs: number;
111
+ jitter: JitterRange | null;
112
+ }
113
+ /** Reduce one schedule spec to `{ intervalMs, jitter }`, or null when invalid. */
114
+ declare function normalize(s: Schedule): Spec | null;
115
+ /** `setTimeout` that survives delays beyond the 32-bit ceiling. Returns a cancel. */
116
+ declare function longTimeout(fn: () => void, delay: number): () => void;
117
+ /** Every registered habit (active or paused), in creation order. */
118
+ declare function listHabits(): HabitController[];
119
+ /** Look up a registered habit by its id. */
120
+ declare function getHabit(id: string): HabitController | undefined;
121
+ /** Subscribe to registry changes (a habit added or removed). Returns unsubscribe. */
122
+ declare function subscribeHabits(listener: () => void): () => void;
123
+ /** Destroy every registered habit (stops timers and empties the registry). */
124
+ declare function clearHabits(): void;
125
+ /**
126
+ * Create a framework-agnostic habit scheduler. The returned controller is
127
+ * registered so it can be found via {@link listHabits} / {@link getHabit}, and
128
+ * removed with `controller.destroy()`.
129
+ *
130
+ * @example
131
+ * const job = createHabit(() => console.log('tick'), { every: '2h ~ 5m' })
132
+ * job.pause()
133
+ */
134
+ declare function createHabit(callback: () => void | Promise<void>, options: HabitOptions): HabitController;
135
+
136
+ export { clearHabits, createHabit, dur, getHabit, listHabits, longTimeout, normalize, resolveJitter, subscribeHabits };
137
+ export type { ControlFlags, Duration, HabitController, HabitOptions, HabitSummary, Jitter, Period, Schedule };
@@ -0,0 +1 @@
1
+ const F={minute:6e4,hour:36e5,day:864e5,week:6048e5,month:2629746e3,year:315576e5},g=2147483647,I={ms:1,sec:1e3,min:6e4,mo:2629746e3,hr:36e5,w:6048e5,s:1e3,m:6e4,h:36e5,d:864e5,y:315576e5},T=/(\d+(?:\.\d+)?)\s*(ms|sec|min|mo|hr|[smhdwy])/g,J=/~|\+\/?-|±/;function u(t){if(typeof t=="number")return t;if(typeof t!="string")return 0;let n=0,r;for(T.lastIndex=0,r=T.exec(t);r!=null;)n+=Number.parseFloat(r[1])*I[r[2]],r=T.exec(t);return n}function w(t){if(t==null)return null;if(typeof t=="number"||typeof t=="string"){const n=u(t);return n>0?{min:0,max:n}:null}return Array.isArray(t)?{min:u(t[0]),max:u(t[1])}:{min:u(t.min??0),max:u(t.max)}}function N(t){let n=0,r=w(t.jitter);if("every"in t&&t.every!=null)if(typeof t.every=="string"&&J.test(t.every)){const[s,a]=t.every.split(J);n=u(s.trim()),r||(r=w(a.trim()))}else n=u(t.every);else"times"in t&&t.times&&t.per&&F[t.per]&&(n=F[t.per]/t.times);return n>0&&Number.isFinite(n)?{intervalMs:n,jitter:r}:null}function R(t,n){let r,s=Math.max(0,n),a=!1;const m=()=>{a||(s<=g?r=setTimeout(t,s):(s-=g,r=setTimeout(m,g)))};return m(),()=>{a=!0,clearTimeout(r)}}function z(t){return("habits"in t?t.habits:[t]).map(N).filter(n=>n!=null).map(n=>({...n,anchor:0,count:0,nextTs:null,cancel:null}))}let O=0;const p=new Map,M=new Set;function H(){for(const t of M)t()}function P(){return[...p.values()]}function $(t){return p.get(t)}function q(t){return M.add(t),()=>{M.delete(t)}}function B(){for(const t of[...p.values()])t.destroy()}function G(t,n){const r=n,{immediate:s=!1,autoStart:a=!0,random:m=Math.random}=r,h=r.id??`h${++O}`;let j=r.name,f=z(r),x=0,i=!1,A=null;const b=new Set,c=()=>{for(const e of b)e()},C=(e,o)=>e+m()*(o-e),v=()=>{let e=null;for(const o of f)o.nextTs!=null&&(e==null||o.nextTs<e)&&(e=o.nextTs);A=e==null?null:new Date(e)},D=()=>{x++,c();try{const e=t();e&&typeof e.then=="function"&&e.catch(()=>{})}catch{}},E=e=>{if(!e.jitter)return 0;let o=C(e.jitter.min,e.jitter.max);const l=e.intervalMs*.49;return o>l&&(o=l),(m()<.5?-1:1)*o},S=e=>{e.count++;const o=e.anchor+e.count*e.intervalMs+E(e);e.nextTs=o,v(),c(),e.cancel=R(()=>{D(),i&&S(e)},o-Date.now())},d=(e=!1)=>{if(i)return;const o=Date.now();for(const l of f)l.anchor=o,l.count=0;i=!0,c(),e&&D();for(const l of f)S(l)},y=()=>{for(const e of f)e.cancel?.(),e.cancel=null,e.nextTs=null;i=!1,v(),c()},k={id:h,get name(){return j},get counter(){return x},get isActive(){return i},get nextRun(){return A},start:d,stop:y,pause:()=>{i&&y()},resume:()=>{i||d(!1)},reset:()=>{x=0,c(),i&&(y(),d(!1))},update:e=>{const o=i;i&&y(),e.name!==void 0&&(j=e.name),f=z(e),o?d(!1):(v(),c()),H()},destroy:()=>{y(),p.delete(h),H()},subscribe:e=>(b.add(e),()=>{b.delete(e)})};return p.set(h,k),H(),a&&d(s),k}export{B as clearHabits,G as createHabit,u as dur,$ as getHabit,P as listHabits,R as longTimeout,N as normalize,w as resolveJitter,q as subscribeHabits};
@@ -0,0 +1 @@
1
+ "use strict";const core_index=require("../core/index.cjs");exports.clearHabits=core_index.clearHabits,exports.createHabit=core_index.createHabit,exports.dur=core_index.dur,exports.getHabit=core_index.getHabit,exports.listHabits=core_index.listHabits,exports.longTimeout=core_index.longTimeout,exports.normalize=core_index.normalize,exports.resolveJitter=core_index.resolveJitter,exports.subscribeHabits=core_index.subscribeHabits;
@@ -0,0 +1 @@
1
+ export { ControlFlags, Duration, HabitController, HabitOptions, HabitSummary, Jitter, Period, Schedule, clearHabits, createHabit, dur, getHabit, listHabits, longTimeout, normalize, resolveJitter, subscribeHabits } from '../core/index.cjs';
@@ -0,0 +1 @@
1
+ export { ControlFlags, Duration, HabitController, HabitOptions, HabitSummary, Jitter, Period, Schedule, clearHabits, createHabit, dur, getHabit, listHabits, longTimeout, normalize, resolveJitter, subscribeHabits } from '../core/index.mjs';
@@ -0,0 +1 @@
1
+ export { ControlFlags, Duration, HabitController, HabitOptions, HabitSummary, Jitter, Period, Schedule, clearHabits, createHabit, dur, getHabit, listHabits, longTimeout, normalize, resolveJitter, subscribeHabits } from '../core/index.js';
@@ -0,0 +1 @@
1
+ export{clearHabits,createHabit,dur,getHabit,listHabits,longTimeout,normalize,resolveJitter,subscribeHabits}from"../core/index.mjs";
@@ -0,0 +1 @@
1
+ "use strict";const react=require("react"),core_index=require("../core/index.cjs");function useHabit(o,s){const n=react.useRef(o);n.current=o;const a=JSON.stringify(s,(t,i)=>typeof i=="function"?void 0:i),r=react.useRef(s);r.current=s;const[u,e]=react.useState({counter:0,isActive:!1,nextRun:null}),c=react.useRef(null);react.useEffect(()=>{const t=core_index.createHabit(async()=>n.current(),{...r.current,autoStart:!0});c.current=t;const i=()=>{e({counter:t.counter,isActive:t.isActive,nextRun:t.nextRun})},R=t.subscribe(i);return i(),()=>{R(),t.destroy(),c.current=null}},[a]);const f=react.useCallback(()=>c.current?.pause(),[]),l=react.useCallback(()=>c.current?.resume(),[]),x=react.useCallback(()=>c.current?.reset(),[]),b={counter:u.counter,nextRun:u.nextRun};return s.controls?{...b,isActive:u.isActive,pause:f,resume:l,reset:x}:b}function useHabits(){const[o,s]=react.useState([]);return react.useEffect(()=>{let n=[];const a=()=>{s(core_index.listHabits().map(e=>({id:e.id,name:e.name,isActive:e.isActive,counter:e.counter,nextRun:e.nextRun})))},r=()=>{for(const e of n)e();n=core_index.listHabits().map(e=>e.subscribe(a)),a()};r();const u=core_index.subscribeHabits(r);return()=>{u();for(const e of n)e()}},[]),o}exports.useHabit=useHabit,exports.useHabits=useHabits;
@@ -0,0 +1,61 @@
1
+ import { Schedule, HabitSummary } from '../core/index.cjs';
2
+ export { Duration, Jitter, Period } from '../core/index.cjs';
3
+
4
+ interface ReactControlFlags {
5
+ /** Fire once immediately on start (counts toward `counter`). */
6
+ immediate?: boolean;
7
+ /** Expose `pause`, `resume`, `reset`, `isActive` on the return value. */
8
+ controls?: boolean;
9
+ /** Random source in `[0, 1)`. Defaults to `Math.random`. */
10
+ random?: () => number;
11
+ }
12
+ /** A single inline schedule, or an explicit list of overlapping habits. */
13
+ type UseHabitOptions = ReactControlFlags & (Schedule | {
14
+ habits: Schedule[];
15
+ });
16
+ interface HabitBase {
17
+ /** Total number of times the callback has fired. */
18
+ counter: number;
19
+ /** Earliest upcoming fire across all habits, or `null` when stopped. */
20
+ nextRun: Date | null;
21
+ }
22
+ interface HabitControls {
23
+ isActive: boolean;
24
+ pause: () => void;
25
+ resume: () => void;
26
+ /** Reset `counter` to 0 and, if active, restart all habits from now. */
27
+ reset: () => void;
28
+ }
29
+ /**
30
+ * Schedule a callback on randomized recurring intervals — a "habit" engine.
31
+ *
32
+ * Accurate by default (evenly spaced, anchored to start time, no drift).
33
+ * Add `jitter` to perturb each fire by a bounded random amount.
34
+ *
35
+ * The schedule is captured once when the component mounts; the callback is
36
+ * always read fresh, so closing over changing props is safe. Control members
37
+ * (`pause`/`resume`/`reset`/`isActive`) are returned only with `controls: true`
38
+ * — expressed via overloads, so the return type is exact with no casting.
39
+ *
40
+ * @example
41
+ * const { counter, nextRun, pause } = useHabit(act, {
42
+ * controls: true,
43
+ * every: '20s ~ 4s',
44
+ * })
45
+ */
46
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions & {
47
+ controls: true;
48
+ }): HabitBase & HabitControls;
49
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions): HabitBase;
50
+ /**
51
+ * Reactively list every registered habit (from `createHabit` / `useHabit`).
52
+ * Re-renders as habits are added, removed, or change state — a ready-made
53
+ * management view.
54
+ *
55
+ * @example
56
+ * const habits = useHabits() // [{ id, name, isActive, counter, nextRun }, …]
57
+ */
58
+ declare function useHabits(): HabitSummary[];
59
+
60
+ export { HabitSummary, Schedule, useHabit, useHabits };
61
+ export type { HabitBase, HabitControls, ReactControlFlags, UseHabitOptions };
@@ -0,0 +1,61 @@
1
+ import { Schedule, HabitSummary } from '../core/index.mjs';
2
+ export { Duration, Jitter, Period } from '../core/index.mjs';
3
+
4
+ interface ReactControlFlags {
5
+ /** Fire once immediately on start (counts toward `counter`). */
6
+ immediate?: boolean;
7
+ /** Expose `pause`, `resume`, `reset`, `isActive` on the return value. */
8
+ controls?: boolean;
9
+ /** Random source in `[0, 1)`. Defaults to `Math.random`. */
10
+ random?: () => number;
11
+ }
12
+ /** A single inline schedule, or an explicit list of overlapping habits. */
13
+ type UseHabitOptions = ReactControlFlags & (Schedule | {
14
+ habits: Schedule[];
15
+ });
16
+ interface HabitBase {
17
+ /** Total number of times the callback has fired. */
18
+ counter: number;
19
+ /** Earliest upcoming fire across all habits, or `null` when stopped. */
20
+ nextRun: Date | null;
21
+ }
22
+ interface HabitControls {
23
+ isActive: boolean;
24
+ pause: () => void;
25
+ resume: () => void;
26
+ /** Reset `counter` to 0 and, if active, restart all habits from now. */
27
+ reset: () => void;
28
+ }
29
+ /**
30
+ * Schedule a callback on randomized recurring intervals — a "habit" engine.
31
+ *
32
+ * Accurate by default (evenly spaced, anchored to start time, no drift).
33
+ * Add `jitter` to perturb each fire by a bounded random amount.
34
+ *
35
+ * The schedule is captured once when the component mounts; the callback is
36
+ * always read fresh, so closing over changing props is safe. Control members
37
+ * (`pause`/`resume`/`reset`/`isActive`) are returned only with `controls: true`
38
+ * — expressed via overloads, so the return type is exact with no casting.
39
+ *
40
+ * @example
41
+ * const { counter, nextRun, pause } = useHabit(act, {
42
+ * controls: true,
43
+ * every: '20s ~ 4s',
44
+ * })
45
+ */
46
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions & {
47
+ controls: true;
48
+ }): HabitBase & HabitControls;
49
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions): HabitBase;
50
+ /**
51
+ * Reactively list every registered habit (from `createHabit` / `useHabit`).
52
+ * Re-renders as habits are added, removed, or change state — a ready-made
53
+ * management view.
54
+ *
55
+ * @example
56
+ * const habits = useHabits() // [{ id, name, isActive, counter, nextRun }, …]
57
+ */
58
+ declare function useHabits(): HabitSummary[];
59
+
60
+ export { HabitSummary, Schedule, useHabit, useHabits };
61
+ export type { HabitBase, HabitControls, ReactControlFlags, UseHabitOptions };