@justin06lee/subaru 0.1.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,178 @@
1
+ // src/styles.ts
2
+ var CARD_TOKENS = [
3
+ "accent",
4
+ "accent-text",
5
+ "bg",
6
+ "text",
7
+ "muted",
8
+ "border",
9
+ "shadow",
10
+ "fill",
11
+ "fill-hover",
12
+ "font",
13
+ "radius",
14
+ "button-radius",
15
+ "padding",
16
+ "gap",
17
+ "width",
18
+ "offset",
19
+ "z"
20
+ ];
21
+ var CARD_POSITIONS = ["bottom-right", "bottom-left", "bottom-center", "top-right", "top-left", "top-center"];
22
+ var DARK = `
23
+ --subaru-accent:#f5c542;
24
+ --subaru-accent-text:#1b1400;
25
+ --subaru-bg:#15171d;
26
+ --subaru-text:#eef1f7;
27
+ --subaru-muted:#a4abba;
28
+ --subaru-border:rgba(255,255,255,.09);
29
+ --subaru-shadow:0 14px 40px rgba(0,0,0,.5);
30
+ --subaru-fill:rgba(255,255,255,.08);
31
+ --subaru-fill-hover:rgba(255,255,255,.15);
32
+ `;
33
+ var LIGHT = `
34
+ --subaru-accent:#f5c542;
35
+ --subaru-accent-text:#231a00;
36
+ --subaru-bg:#fff;
37
+ --subaru-text:#15171d;
38
+ --subaru-muted:#61697a;
39
+ --subaru-border:rgba(0,0,0,.08);
40
+ --subaru-shadow:0 14px 40px rgba(0,0,0,.16);
41
+ --subaru-fill:rgba(0,0,0,.055);
42
+ --subaru-fill-hover:rgba(0,0,0,.1);
43
+ `;
44
+ var CARD_CSS = `
45
+ .subaru{
46
+ ${DARK}
47
+ --subaru-font:500 14px/1.45 system-ui,-apple-system,"Segoe UI",sans-serif;
48
+ --subaru-radius:14px;
49
+ --subaru-button-radius:9px;
50
+ --subaru-padding:14px;
51
+ --subaru-gap:12px;
52
+ --subaru-width:23rem;
53
+ --subaru-offset:16px;
54
+ --subaru-z:2147483000;
55
+ --subaru-tx:0;
56
+ --subaru-ty:8px;
57
+ position:fixed;
58
+ right:var(--subaru-offset);
59
+ bottom:var(--subaru-offset);
60
+ z-index:var(--subaru-z);
61
+ box-sizing:border-box;
62
+ width:min(var(--subaru-width),calc(100vw - var(--subaru-offset) * 2));
63
+ padding:var(--subaru-padding);
64
+ border:1px solid var(--subaru-border);
65
+ border-radius:var(--subaru-radius);
66
+ background:var(--subaru-bg);
67
+ color:var(--subaru-text);
68
+ font:var(--subaru-font);
69
+ box-shadow:var(--subaru-shadow);
70
+ display:flex;
71
+ flex-direction:column;
72
+ gap:var(--subaru-gap);
73
+ /* Runs when the element enters the document. Both cards keep one element
74
+ across state changes, so a progress tick never replays or cuts it. */
75
+ animation:subaru-in .22s cubic-bezier(.2,.8,.3,1);
76
+ }
77
+ @media (prefers-color-scheme: light){.subaru:where([data-subaru-theme="auto"]){${LIGHT}}}
78
+ .subaru:where([data-subaru-theme="light"]){${LIGHT}}
79
+ .subaru:where([data-subaru-theme="dark"]){${DARK}}
80
+ .subaru:where([data-subaru-position^="top"]){top:var(--subaru-offset);bottom:auto;--subaru-ty:-8px}
81
+ .subaru:where([data-subaru-position$="left"]){left:var(--subaru-offset);right:auto}
82
+ .subaru:where([data-subaru-position$="center"]){left:50%;right:auto;translate:-50% 0;--subaru-tx:-50%}
83
+ @keyframes subaru-in{from{opacity:0;translate:var(--subaru-tx) var(--subaru-ty)}to{opacity:1;translate:var(--subaru-tx) 0}}
84
+
85
+ .subaru-body{display:flex;flex-direction:column;gap:8px;min-width:0}
86
+ .subaru-text{font:inherit;text-wrap:pretty;overflow-wrap:anywhere}
87
+ /* An error can be a whole tool's output; keep the card a card. */
88
+ .subaru-error .subaru-text{max-height:7.5rem;overflow:auto;overscroll-behavior:contain;white-space:pre-wrap}
89
+ .subaru-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
90
+
91
+ .subaru-button{
92
+ appearance:none;
93
+ margin:0;
94
+ border:0;
95
+ border-radius:var(--subaru-button-radius);
96
+ padding:8px 13px;
97
+ font:inherit;
98
+ font-weight:600;
99
+ line-height:1.2;
100
+ color:inherit;
101
+ background:var(--subaru-fill);
102
+ cursor:pointer;
103
+ transition:background .12s ease,filter .12s ease;
104
+ }
105
+ .subaru-button:hover{background:var(--subaru-fill-hover)}
106
+ .subaru-button:active{filter:brightness(.94)}
107
+ .subaru-primary{background:var(--subaru-accent);color:var(--subaru-accent-text)}
108
+ .subaru-primary:hover{background:var(--subaru-accent);filter:brightness(1.07)}
109
+ .subaru :focus-visible{outline:2px solid var(--subaru-accent);outline-offset:2px}
110
+
111
+ .subaru-link{color:var(--subaru-muted);margin-right:auto;text-underline-offset:2px}
112
+ .subaru-link:hover{color:var(--subaru-text)}
113
+ .subaru-notes-toggle{
114
+ appearance:none;
115
+ align-self:flex-start;
116
+ margin:0;
117
+ border:0;
118
+ padding:0;
119
+ background:none;
120
+ font:inherit;
121
+ font-size:13px;
122
+ font-weight:500;
123
+ color:var(--subaru-muted);
124
+ text-align:left;
125
+ text-decoration:underline;
126
+ text-underline-offset:2px;
127
+ cursor:pointer;
128
+ }
129
+ .subaru-notes-toggle:hover{color:var(--subaru-text)}
130
+ .subaru-notes{
131
+ margin:0;
132
+ max-height:9.5rem;
133
+ overflow:auto;
134
+ overscroll-behavior:contain;
135
+ padding:9px 11px;
136
+ border-radius:calc(var(--subaru-button-radius) - 1px);
137
+ background:var(--subaru-fill);
138
+ color:var(--subaru-muted);
139
+ font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;
140
+ white-space:pre-wrap;
141
+ }
142
+
143
+ .subaru-bar{height:5px;border-radius:999px;background:var(--subaru-fill);overflow:hidden}
144
+ .subaru-bar-fill{height:100%;border-radius:inherit;background:var(--subaru-accent);transition:width .2s ease}
145
+ .subaru-bar-indeterminate .subaru-bar-fill{width:40%;animation:subaru-slide 1.3s ease-in-out infinite}
146
+ @keyframes subaru-slide{from{translate:-105% 0}to{translate:265% 0}}
147
+
148
+ @media (prefers-reduced-motion: reduce){
149
+ .subaru,.subaru-bar-fill,.subaru-bar-indeterminate .subaru-bar-fill{animation:none;transition:none}
150
+ }
151
+ `;
152
+ var injected = false;
153
+ function injectStyles() {
154
+ if (injected || typeof document === "undefined") return;
155
+ injected = true;
156
+ const style = document.createElement("style");
157
+ style.setAttribute("data-subaru", "");
158
+ document.head.prepend(style);
159
+ style.textContent = CARD_CSS;
160
+ }
161
+ function cardVars(options) {
162
+ const out = {};
163
+ const add = (key, value) => {
164
+ out[key.startsWith("--") ? key : `--subaru-${key}`] = value;
165
+ };
166
+ if (options.accent) add("accent", options.accent);
167
+ for (const [key, value] of Object.entries(options.vars ?? {})) if (value != null) add(key, value);
168
+ return out;
169
+ }
170
+
171
+ export {
172
+ CARD_TOKENS,
173
+ CARD_POSITIONS,
174
+ CARD_CSS,
175
+ injectStyles,
176
+ cardVars
177
+ };
178
+ //# sourceMappingURL=chunk-POOH3Z7G.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/styles.ts"],"sourcesContent":["/**\n * The update card's default look, shared by the React and the vanilla card.\n *\n * Three things make it editable without forking. Every value is a CSS custom\n * property on `.subaru`, so `--subaru-accent: rebeccapurple` is the whole\n * edit. This stylesheet is inserted first in <head>, and its rules are plain\n * one-class selectors, so a `.subaru { ... }` rule of your own comes later,\n * wins on the tie, and needs no `!important` — while an app's bare\n * `button { ... }` styles, being weaker than a class, cannot reach inside the\n * card. And the theme and position hooks sit in `:where(...)`, keeping them\n * at one class too, so overriding a token on `.subaru` beats them.\n * `unstyled` drops the stylesheet altogether and leaves the class names.\n */\n\n/** The tokens the card reads, without the `--subaru-` prefix. */\nexport const CARD_TOKENS = [\n 'accent',\n 'accent-text',\n 'bg',\n 'text',\n 'muted',\n 'border',\n 'shadow',\n 'fill',\n 'fill-hover',\n 'font',\n 'radius',\n 'button-radius',\n 'padding',\n 'gap',\n 'width',\n 'offset',\n 'z',\n] as const;\n\nexport type CardToken = (typeof CARD_TOKENS)[number];\n\n/** Where the card sits. */\nexport const CARD_POSITIONS = ['bottom-right', 'bottom-left', 'bottom-center', 'top-right', 'top-left', 'top-center'] as const;\n\nexport type CardPosition = (typeof CARD_POSITIONS)[number];\n\n/** 'auto' follows prefers-color-scheme. */\nexport type CardTheme = 'auto' | 'dark' | 'light';\n\nconst DARK = `\n --subaru-accent:#f5c542;\n --subaru-accent-text:#1b1400;\n --subaru-bg:#15171d;\n --subaru-text:#eef1f7;\n --subaru-muted:#a4abba;\n --subaru-border:rgba(255,255,255,.09);\n --subaru-shadow:0 14px 40px rgba(0,0,0,.5);\n --subaru-fill:rgba(255,255,255,.08);\n --subaru-fill-hover:rgba(255,255,255,.15);\n`;\n\nconst LIGHT = `\n --subaru-accent:#f5c542;\n --subaru-accent-text:#231a00;\n --subaru-bg:#fff;\n --subaru-text:#15171d;\n --subaru-muted:#61697a;\n --subaru-border:rgba(0,0,0,.08);\n --subaru-shadow:0 14px 40px rgba(0,0,0,.16);\n --subaru-fill:rgba(0,0,0,.055);\n --subaru-fill-hover:rgba(0,0,0,.1);\n`;\n\nexport const CARD_CSS = `\n.subaru{\n${DARK}\n --subaru-font:500 14px/1.45 system-ui,-apple-system,\"Segoe UI\",sans-serif;\n --subaru-radius:14px;\n --subaru-button-radius:9px;\n --subaru-padding:14px;\n --subaru-gap:12px;\n --subaru-width:23rem;\n --subaru-offset:16px;\n --subaru-z:2147483000;\n --subaru-tx:0;\n --subaru-ty:8px;\n position:fixed;\n right:var(--subaru-offset);\n bottom:var(--subaru-offset);\n z-index:var(--subaru-z);\n box-sizing:border-box;\n width:min(var(--subaru-width),calc(100vw - var(--subaru-offset) * 2));\n padding:var(--subaru-padding);\n border:1px solid var(--subaru-border);\n border-radius:var(--subaru-radius);\n background:var(--subaru-bg);\n color:var(--subaru-text);\n font:var(--subaru-font);\n box-shadow:var(--subaru-shadow);\n display:flex;\n flex-direction:column;\n gap:var(--subaru-gap);\n /* Runs when the element enters the document. Both cards keep one element\n across state changes, so a progress tick never replays or cuts it. */\n animation:subaru-in .22s cubic-bezier(.2,.8,.3,1);\n}\n@media (prefers-color-scheme: light){.subaru:where([data-subaru-theme=\"auto\"]){${LIGHT}}}\n.subaru:where([data-subaru-theme=\"light\"]){${LIGHT}}\n.subaru:where([data-subaru-theme=\"dark\"]){${DARK}}\n.subaru:where([data-subaru-position^=\"top\"]){top:var(--subaru-offset);bottom:auto;--subaru-ty:-8px}\n.subaru:where([data-subaru-position$=\"left\"]){left:var(--subaru-offset);right:auto}\n.subaru:where([data-subaru-position$=\"center\"]){left:50%;right:auto;translate:-50% 0;--subaru-tx:-50%}\n@keyframes subaru-in{from{opacity:0;translate:var(--subaru-tx) var(--subaru-ty)}to{opacity:1;translate:var(--subaru-tx) 0}}\n\n.subaru-body{display:flex;flex-direction:column;gap:8px;min-width:0}\n.subaru-text{font:inherit;text-wrap:pretty;overflow-wrap:anywhere}\n/* An error can be a whole tool's output; keep the card a card. */\n.subaru-error .subaru-text{max-height:7.5rem;overflow:auto;overscroll-behavior:contain;white-space:pre-wrap}\n.subaru-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px}\n\n.subaru-button{\n appearance:none;\n margin:0;\n border:0;\n border-radius:var(--subaru-button-radius);\n padding:8px 13px;\n font:inherit;\n font-weight:600;\n line-height:1.2;\n color:inherit;\n background:var(--subaru-fill);\n cursor:pointer;\n transition:background .12s ease,filter .12s ease;\n}\n.subaru-button:hover{background:var(--subaru-fill-hover)}\n.subaru-button:active{filter:brightness(.94)}\n.subaru-primary{background:var(--subaru-accent);color:var(--subaru-accent-text)}\n.subaru-primary:hover{background:var(--subaru-accent);filter:brightness(1.07)}\n.subaru :focus-visible{outline:2px solid var(--subaru-accent);outline-offset:2px}\n\n.subaru-link{color:var(--subaru-muted);margin-right:auto;text-underline-offset:2px}\n.subaru-link:hover{color:var(--subaru-text)}\n.subaru-notes-toggle{\n appearance:none;\n align-self:flex-start;\n margin:0;\n border:0;\n padding:0;\n background:none;\n font:inherit;\n font-size:13px;\n font-weight:500;\n color:var(--subaru-muted);\n text-align:left;\n text-decoration:underline;\n text-underline-offset:2px;\n cursor:pointer;\n}\n.subaru-notes-toggle:hover{color:var(--subaru-text)}\n.subaru-notes{\n margin:0;\n max-height:9.5rem;\n overflow:auto;\n overscroll-behavior:contain;\n padding:9px 11px;\n border-radius:calc(var(--subaru-button-radius) - 1px);\n background:var(--subaru-fill);\n color:var(--subaru-muted);\n font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;\n white-space:pre-wrap;\n}\n\n.subaru-bar{height:5px;border-radius:999px;background:var(--subaru-fill);overflow:hidden}\n.subaru-bar-fill{height:100%;border-radius:inherit;background:var(--subaru-accent);transition:width .2s ease}\n.subaru-bar-indeterminate .subaru-bar-fill{width:40%;animation:subaru-slide 1.3s ease-in-out infinite}\n@keyframes subaru-slide{from{translate:-105% 0}to{translate:265% 0}}\n\n@media (prefers-reduced-motion: reduce){\n .subaru,.subaru-bar-fill,.subaru-bar-indeterminate .subaru-bar-fill{animation:none;transition:none}\n}\n`;\n\nlet injected = false;\n\n/** Add the default stylesheet to the document once. */\nexport function injectStyles(): void {\n if (injected || typeof document === 'undefined') return;\n injected = true;\n const style = document.createElement('style');\n style.setAttribute('data-subaru', '');\n // First in <head>, so a stylesheet of your own comes later and wins even on equal specificity.\n document.head.prepend(style);\n style.textContent = CARD_CSS;\n}\n\n/** For tests. */\nexport function resetStyles(): void {\n injected = false;\n}\n\n/**\n * Token overrides for one card: `{ accent: '#7c5cff', radius: '4px' }`, or\n * any CSS custom property written out in full.\n */\nexport type CardVars = Partial<Record<CardToken, string>> & Record<string, string>;\n\n/** How the card looks, shared by <UpdatePrompt> and mountCard. */\nexport interface CardStyleOptions {\n /** Default 'auto' (follows prefers-color-scheme). */\n theme?: CardTheme;\n /** Default 'bottom-right'. */\n position?: CardPosition;\n /** Shorthand for vars: { accent }. */\n accent?: string;\n /** Token overrides: { radius: '4px', width: '26rem' }. */\n vars?: CardVars;\n /** Drop the built-in look and style the class names yourself. */\n unstyled?: boolean;\n className?: string;\n}\n\n/** The inline custom properties a card carries, from accent and vars. */\nexport function cardVars(options: Pick<CardStyleOptions, 'accent' | 'vars'>): Record<string, string> {\n const out: Record<string, string> = {};\n const add = (key: string, value: string) => {\n out[key.startsWith('--') ? key : `--subaru-${key}`] = value;\n };\n if (options.accent) add('accent', options.accent);\n for (const [key, value] of Object.entries(options.vars ?? {})) if (value != null) add(key, value);\n return out;\n}\n"],"mappings":";AAeO,IAAM,cAAc;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKO,IAAM,iBAAiB,CAAC,gBAAgB,eAAe,iBAAiB,aAAa,YAAY,YAAY;AAOpH,IAAM,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYb,IAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYP,IAAM,WAAW;AAAA;AAAA,EAEtB,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iFA+B2E,KAAK;AAAA,6CACzC,KAAK;AAAA,4CACN,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA0EhD,IAAI,WAAW;AAGR,SAAS,eAAqB;AACnC,MAAI,YAAY,OAAO,aAAa,YAAa;AACjD,aAAW;AACX,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,aAAa,eAAe,EAAE;AAEpC,WAAS,KAAK,QAAQ,KAAK;AAC3B,QAAM,cAAc;AACtB;AA6BO,SAAS,SAAS,SAA4E;AACnG,QAAM,MAA8B,CAAC;AACrC,QAAM,MAAM,CAAC,KAAa,UAAkB;AAC1C,QAAI,IAAI,WAAW,IAAI,IAAI,MAAM,YAAY,GAAG,EAAE,IAAI;AAAA,EACxD;AACA,MAAI,QAAQ,OAAQ,KAAI,UAAU,QAAQ,MAAM;AAChD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,QAAQ,CAAC,CAAC,EAAG,KAAI,SAAS,KAAM,KAAI,KAAK,KAAK;AAChG,SAAO;AACT;","names":[]}
@@ -0,0 +1,261 @@
1
+ // src/core.ts
2
+ var HOUR = 60 * 60 * 1e3;
3
+ var DAY = 24 * HOUR;
4
+ var MIN_TIMER = 1e3;
5
+ function createUpdater(options) {
6
+ const adapter = options.adapter;
7
+ const policy = options.policy ?? "prompt";
8
+ const interval = options.interval ?? adapter.interval ?? DAY;
9
+ const timeout = options.timeout ?? 15e3;
10
+ const storage = options.storage ?? adapter.storage ?? memoryStorage();
11
+ const now = options.now ?? (() => Date.now());
12
+ const schedule = options.schedule ?? defaultSchedule;
13
+ const debug = options.debug ?? (() => {
14
+ });
15
+ const key = `subaru:${adapter.name}`;
16
+ let state = {
17
+ status: "idle",
18
+ policy,
19
+ release: null,
20
+ progress: null,
21
+ error: null,
22
+ skipped: false,
23
+ dismissed: false,
24
+ kind: adapter.kind
25
+ };
26
+ const listeners = /* @__PURE__ */ new Set();
27
+ let cancelTimer = null;
28
+ let started = false;
29
+ let checking = null;
30
+ function set(patch) {
31
+ state = { ...state, ...patch };
32
+ for (const l of listeners) l(state);
33
+ }
34
+ async function load() {
35
+ try {
36
+ const raw = await storage.get(key);
37
+ return raw ? JSON.parse(raw) : {};
38
+ } catch (e) {
39
+ debug(`state unreadable: ${String(e)}`);
40
+ return {};
41
+ }
42
+ }
43
+ async function save(patch) {
44
+ try {
45
+ const current = await load();
46
+ await storage.set(key, JSON.stringify({ ...current, ...patch }));
47
+ } catch (e) {
48
+ debug(`state not saved: ${String(e)}`);
49
+ }
50
+ }
51
+ function armTimer(delay) {
52
+ cancelTimer?.();
53
+ cancelTimer = schedule(() => {
54
+ cancelTimer = null;
55
+ void runCheck(false);
56
+ }, Math.max(delay, MIN_TIMER));
57
+ }
58
+ async function runCheck(force) {
59
+ if (checking) return checking;
60
+ checking = (async () => {
61
+ const persisted = await load();
62
+ if (!force && persisted.nextCheck && persisted.nextCheck > now()) {
63
+ if (persisted.latest && persisted.latest !== persisted.installed && state.status === "idle") {
64
+ set({
65
+ status: "available",
66
+ release: { tag: persisted.latest },
67
+ skipped: persisted.skipped === persisted.latest
68
+ });
69
+ }
70
+ if (started) armTimer(persisted.nextCheck - now());
71
+ return;
72
+ }
73
+ set({ status: "checking", error: null });
74
+ const controller = new AbortController();
75
+ const stopTimeout = schedule(() => controller.abort(), timeout);
76
+ let release;
77
+ try {
78
+ release = await adapter.check(controller.signal);
79
+ } catch (e) {
80
+ stopTimeout();
81
+ debug(`check failed: ${String(e)}`);
82
+ await save({ nextCheck: now() + Math.min(interval, HOUR) });
83
+ set({ status: state.release ? "available" : "idle" });
84
+ if (started) armTimer(Math.min(interval, HOUR));
85
+ return;
86
+ }
87
+ stopTimeout();
88
+ await save({ nextCheck: now() + interval, latest: release?.tag });
89
+ if (started) armTimer(interval);
90
+ if (!release) {
91
+ set({ status: "current", release: null, skipped: false });
92
+ return;
93
+ }
94
+ if (persisted.installed === release.tag) {
95
+ set({ status: "ready", release });
96
+ return;
97
+ }
98
+ const skipped = persisted.skipped === release.tag;
99
+ set({ status: "available", release, skipped });
100
+ if (policy === "auto" && !skipped) {
101
+ await apply(release, false);
102
+ }
103
+ })().finally(() => {
104
+ checking = null;
105
+ });
106
+ return checking;
107
+ }
108
+ async function apply(release, relaunch) {
109
+ set({ status: "installing", progress: null, error: null, dismissed: false });
110
+ try {
111
+ const outcome = await adapter.install(release, {
112
+ relaunch,
113
+ onProgress: (fraction) => set({ progress: fraction })
114
+ });
115
+ await save({ installed: release.tag });
116
+ set({ status: outcome === "restarting" ? "installing" : "ready", progress: outcome === "restarting" ? 1 : null });
117
+ } catch (e) {
118
+ debug(`install failed: ${String(e)}`);
119
+ const message = e instanceof Error ? e.message : String(e);
120
+ if (policy === "auto") {
121
+ await save({ skipped: release.tag });
122
+ set({ status: "available", skipped: true, error: message, progress: null });
123
+ } else {
124
+ set({ status: "error", error: message, progress: null });
125
+ }
126
+ }
127
+ }
128
+ async function ensureRelease() {
129
+ const known = state.release;
130
+ if (known && (known.notes !== void 0 || known.url !== void 0 || known.version !== void 0)) return known;
131
+ const controller = new AbortController();
132
+ const fresh = await adapter.check(controller.signal);
133
+ if (!fresh) {
134
+ set({ status: "current", release: null });
135
+ return null;
136
+ }
137
+ set({ release: fresh });
138
+ return fresh;
139
+ }
140
+ const updater = {
141
+ getState: () => state,
142
+ subscribe(listener) {
143
+ listeners.add(listener);
144
+ return () => {
145
+ listeners.delete(listener);
146
+ };
147
+ },
148
+ start() {
149
+ if (started) return;
150
+ started = true;
151
+ if (policy === "off") {
152
+ debug("policy off");
153
+ return;
154
+ }
155
+ void runCheck(false);
156
+ },
157
+ stop() {
158
+ started = false;
159
+ cancelTimer?.();
160
+ cancelTimer = null;
161
+ },
162
+ async check() {
163
+ await runCheck(true);
164
+ },
165
+ async install() {
166
+ let release;
167
+ try {
168
+ release = await ensureRelease();
169
+ } catch (e) {
170
+ set({ status: "error", error: e instanceof Error ? e.message : String(e) });
171
+ return;
172
+ }
173
+ if (!release) return;
174
+ await apply(release, true);
175
+ },
176
+ async restart() {
177
+ if (adapter.restart) {
178
+ await adapter.restart();
179
+ } else if (state.release) {
180
+ await apply(state.release, true);
181
+ }
182
+ },
183
+ async skip() {
184
+ const tag = state.release?.tag;
185
+ if (!tag) return;
186
+ await save({ skipped: tag });
187
+ set({ skipped: true, dismissed: true });
188
+ },
189
+ dismiss() {
190
+ set({ dismissed: true });
191
+ }
192
+ };
193
+ return updater;
194
+ }
195
+ function defaultSchedule(fn, ms) {
196
+ const id = setTimeout(fn, ms);
197
+ return () => clearTimeout(id);
198
+ }
199
+ function memoryStorage() {
200
+ const map = /* @__PURE__ */ new Map();
201
+ return {
202
+ get: (k) => map.get(k) ?? null,
203
+ set: (k, v) => {
204
+ map.set(k, v);
205
+ }
206
+ };
207
+ }
208
+ function webStorage(area) {
209
+ const fallback = memoryStorage();
210
+ const resolve = () => {
211
+ try {
212
+ return area ?? (typeof localStorage !== "undefined" ? localStorage : null);
213
+ } catch {
214
+ return null;
215
+ }
216
+ };
217
+ return {
218
+ get(k) {
219
+ try {
220
+ return resolve()?.getItem(k) ?? fallback.get(k);
221
+ } catch {
222
+ return fallback.get(k);
223
+ }
224
+ },
225
+ set(k, v) {
226
+ try {
227
+ resolve()?.setItem(k, v);
228
+ } catch {
229
+ }
230
+ return fallback.set(k, v);
231
+ }
232
+ };
233
+ }
234
+ function parsePolicy(value) {
235
+ switch ((value ?? "").trim().toLowerCase()) {
236
+ case "prompt":
237
+ case "ask":
238
+ return "prompt";
239
+ case "notify":
240
+ case "notice":
241
+ return "notify";
242
+ case "auto":
243
+ case "always":
244
+ case "silent":
245
+ return "auto";
246
+ case "off":
247
+ case "never":
248
+ case "0":
249
+ case "false":
250
+ return "off";
251
+ }
252
+ return null;
253
+ }
254
+
255
+ export {
256
+ createUpdater,
257
+ memoryStorage,
258
+ webStorage,
259
+ parsePolicy
260
+ };
261
+ //# sourceMappingURL=chunk-SRQ3JWJK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core.ts"],"sourcesContent":["/**\n * The runtime-independent heart of subaru: the same policy, throttle and\n * memory that the Go module has, driven by an Adapter that knows how a given\n * runtime (a web page, a Tauri window, an Electron main process) finds and\n * applies a release.\n */\n\n/** What to do when a newer release exists. Identical to the Go module. */\nexport type Policy = 'prompt' | 'notify' | 'auto' | 'off';\n\n/** A release newer than the running program. */\nexport interface Release {\n /** Identity of the release: a tag, a version, or a build id. Skips and installs are remembered by it. */\n tag: string;\n /** Human-readable version when it differs from the tag (web builds). */\n version?: string;\n notes?: string;\n url?: string;\n date?: string;\n}\n\nexport type Status =\n | 'idle'\n | 'checking'\n | 'current'\n | 'available'\n | 'installing'\n | 'ready'\n | 'error';\n\nexport interface UpdaterState {\n status: Status;\n policy: Policy;\n release: Release | null;\n /** 0..1 while installing when the runtime reports progress. */\n progress: number | null;\n error: string | null;\n /** The release is one the user skipped: mention it, never ask. */\n skipped: boolean;\n /** Hidden for this session by the user. */\n dismissed: boolean;\n /** How the runtime applies an update, for wording. */\n kind: AdapterKind;\n}\n\nexport type AdapterKind = 'web' | 'tauri' | 'electron' | 'source' | 'custom';\n\nexport interface InstallOptions {\n /** Restart (or reload) as soon as the release is applied. False means \"apply now, take effect on the next start\". */\n relaunch: boolean;\n onProgress: (fraction: number | null) => void;\n}\n\n/** What install did: the program is going away, or the update waits for the next start. */\nexport type InstallOutcome = 'restarting' | 'ready';\n\nexport interface Adapter {\n kind: AdapterKind;\n /** Stable name used to key persisted state. */\n name: string;\n /** Resolve a newer release, or null when the program is current. */\n check(signal: AbortSignal): Promise<Release | null>;\n /** Download and apply the release. */\n install(release: Release, options: InstallOptions): Promise<InstallOutcome>;\n /** Restart to finish a 'ready' update. */\n restart?(): Promise<void>;\n /** Suggested minimum time between checks, in milliseconds. */\n interval?: number;\n /** Where to remember the next check time and answered releases. */\n storage?: StateStorage;\n}\n\n/** The persistence subaru needs: two string operations, sync or async. */\nexport interface StateStorage {\n get(key: string): string | null | Promise<string | null>;\n set(key: string, value: string): void | Promise<void>;\n}\n\nexport interface UpdaterOptions {\n adapter: Adapter;\n policy?: Policy;\n /** Minimum time between checks, in milliseconds. Defaults to the adapter's suggestion, then 24 hours. */\n interval?: number;\n /** Abort a check after this long. Default 15 seconds. */\n timeout?: number;\n storage?: StateStorage;\n /** Diagnostics sink. */\n debug?: (message: string) => void;\n /** Injection points for tests. */\n now?: () => number;\n schedule?: (fn: () => void, ms: number) => () => void;\n}\n\n/** The surface a UI talks to. Both the core Updater and the Electron renderer proxy provide it. */\nexport interface UpdaterLike {\n getState(): UpdaterState;\n subscribe(listener: (state: UpdaterState) => void): () => void;\n /** Force a check now, ignoring the interval. */\n check(): Promise<void>;\n /** Install the known release and restart. */\n install(): Promise<void>;\n /** Restart to finish a 'ready' update. */\n restart(): Promise<void>;\n /** Never ask about this release again. */\n skip(): Promise<void>;\n /** Hide the notice for this session. */\n dismiss(): void;\n}\n\nexport interface Updater extends UpdaterLike {\n /** Begin background checks according to the policy. Safe to call once. */\n start(): void;\n stop(): void;\n}\n\ninterface Persisted {\n nextCheck?: number;\n latest?: string;\n skipped?: string;\n installed?: string;\n}\n\nconst HOUR = 60 * 60 * 1000;\nconst DAY = 24 * HOUR;\n/** Timers never fire more often than this, whatever the interval says. */\nconst MIN_TIMER = 1000;\n\nexport function createUpdater(options: UpdaterOptions): Updater {\n const adapter = options.adapter;\n const policy = options.policy ?? 'prompt';\n const interval = options.interval ?? adapter.interval ?? DAY;\n const timeout = options.timeout ?? 15_000;\n const storage = options.storage ?? adapter.storage ?? memoryStorage();\n const now = options.now ?? (() => Date.now());\n const schedule = options.schedule ?? defaultSchedule;\n const debug = options.debug ?? (() => {});\n const key = `subaru:${adapter.name}`;\n\n let state: UpdaterState = {\n status: 'idle',\n policy,\n release: null,\n progress: null,\n error: null,\n skipped: false,\n dismissed: false,\n kind: adapter.kind,\n };\n const listeners = new Set<(s: UpdaterState) => void>();\n let cancelTimer: (() => void) | null = null;\n let started = false;\n let checking: Promise<void> | null = null;\n\n function set(patch: Partial<UpdaterState>) {\n state = { ...state, ...patch };\n for (const l of listeners) l(state);\n }\n\n async function load(): Promise<Persisted> {\n try {\n const raw = await storage.get(key);\n return raw ? (JSON.parse(raw) as Persisted) : {};\n } catch (e) {\n debug(`state unreadable: ${String(e)}`);\n return {};\n }\n }\n\n async function save(patch: Partial<Persisted>) {\n try {\n const current = await load();\n await storage.set(key, JSON.stringify({ ...current, ...patch }));\n } catch (e) {\n debug(`state not saved: ${String(e)}`);\n }\n }\n\n function armTimer(delay: number) {\n cancelTimer?.();\n cancelTimer = schedule(() => {\n cancelTimer = null;\n void runCheck(false);\n }, Math.max(delay, MIN_TIMER));\n }\n\n /** One check. `force` ignores the interval (a user asked). */\n async function runCheck(force: boolean): Promise<void> {\n if (checking) return checking;\n checking = (async () => {\n const persisted = await load();\n if (!force && persisted.nextCheck && persisted.nextCheck > now()) {\n // Not due. Surface what the last check found so the notice survives reloads.\n if (persisted.latest && persisted.latest !== persisted.installed && state.status === 'idle') {\n // We only know the tag; the adapter is asked again when the user acts.\n set({\n status: 'available',\n release: { tag: persisted.latest },\n skipped: persisted.skipped === persisted.latest,\n });\n }\n if (started) armTimer(persisted.nextCheck - now());\n return;\n }\n set({ status: 'checking', error: null });\n const controller = new AbortController();\n const stopTimeout = schedule(() => controller.abort(), timeout);\n let release: Release | null;\n try {\n release = await adapter.check(controller.signal);\n } catch (e) {\n stopTimeout();\n debug(`check failed: ${String(e)}`);\n await save({ nextCheck: now() + Math.min(interval, HOUR) });\n set({ status: state.release ? 'available' : 'idle' });\n if (started) armTimer(Math.min(interval, HOUR));\n return;\n }\n stopTimeout();\n await save({ nextCheck: now() + interval, latest: release?.tag });\n if (started) armTimer(interval);\n if (!release) {\n set({ status: 'current', release: null, skipped: false });\n return;\n }\n if (persisted.installed === release.tag) {\n set({ status: 'ready', release });\n return;\n }\n const skipped = persisted.skipped === release.tag;\n set({ status: 'available', release, skipped });\n if (policy === 'auto' && !skipped) {\n await apply(release, false);\n }\n })().finally(() => {\n checking = null;\n });\n return checking;\n }\n\n /** Download and apply. Errors land in state, never throw. */\n async function apply(release: Release, relaunch: boolean): Promise<void> {\n set({ status: 'installing', progress: null, error: null, dismissed: false });\n try {\n const outcome = await adapter.install(release, {\n relaunch,\n onProgress: (fraction) => set({ progress: fraction }),\n });\n await save({ installed: release.tag });\n set({ status: outcome === 'restarting' ? 'installing' : 'ready', progress: outcome === 'restarting' ? 1 : null });\n } catch (e) {\n debug(`install failed: ${String(e)}`);\n const message = e instanceof Error ? e.message : String(e);\n if (policy === 'auto') {\n // Do not retry every check; mention it instead.\n await save({ skipped: release.tag });\n set({ status: 'available', skipped: true, error: message, progress: null });\n } else {\n set({ status: 'error', error: message, progress: null });\n }\n }\n }\n\n /** Make sure we hold a full release (a cached notice only has the tag). */\n async function ensureRelease(): Promise<Release | null> {\n const known = state.release;\n if (known && (known.notes !== undefined || known.url !== undefined || known.version !== undefined)) return known;\n const controller = new AbortController();\n const fresh = await adapter.check(controller.signal);\n if (!fresh) {\n set({ status: 'current', release: null });\n return null;\n }\n set({ release: fresh });\n return fresh;\n }\n\n const updater: Updater = {\n getState: () => state,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n start() {\n if (started) return;\n started = true;\n if (policy === 'off') {\n debug('policy off');\n return;\n }\n void runCheck(false);\n },\n stop() {\n started = false;\n cancelTimer?.();\n cancelTimer = null;\n },\n async check() {\n await runCheck(true);\n },\n async install() {\n let release: Release | null;\n try {\n release = await ensureRelease();\n } catch (e) {\n set({ status: 'error', error: e instanceof Error ? e.message : String(e) });\n return;\n }\n if (!release) return;\n await apply(release, true);\n },\n async restart() {\n if (adapter.restart) {\n await adapter.restart();\n } else if (state.release) {\n await apply(state.release, true);\n }\n },\n async skip() {\n const tag = state.release?.tag;\n if (!tag) return;\n await save({ skipped: tag });\n set({ skipped: true, dismissed: true });\n },\n dismiss() {\n set({ dismissed: true });\n },\n };\n return updater;\n}\n\nfunction defaultSchedule(fn: () => void, ms: number): () => void {\n const id = setTimeout(fn, ms);\n return () => clearTimeout(id);\n}\n\n/** State that lives only as long as the process. */\nexport function memoryStorage(): StateStorage {\n const map = new Map<string, string>();\n return {\n get: (k) => map.get(k) ?? null,\n set: (k, v) => {\n map.set(k, v);\n },\n };\n}\n\n/** Browser storage that tolerates private mode and quota errors. */\nexport function webStorage(area?: Storage): StateStorage {\n const fallback = memoryStorage();\n const resolve = () => {\n try {\n return area ?? (typeof localStorage !== 'undefined' ? localStorage : null);\n } catch {\n return null;\n }\n };\n return {\n get(k) {\n try {\n return resolve()?.getItem(k) ?? fallback.get(k);\n } catch {\n return fallback.get(k);\n }\n },\n set(k, v) {\n try {\n resolve()?.setItem(k, v);\n } catch {\n /* quota or private mode */\n }\n return fallback.set(k, v);\n },\n };\n}\n\n/** Policy from a string, for environment variables and query flags. */\nexport function parsePolicy(value: string | undefined | null): Policy | null {\n switch ((value ?? '').trim().toLowerCase()) {\n case 'prompt':\n case 'ask':\n return 'prompt';\n case 'notify':\n case 'notice':\n return 'notify';\n case 'auto':\n case 'always':\n case 'silent':\n return 'auto';\n case 'off':\n case 'never':\n case '0':\n case 'false':\n return 'off';\n }\n return null;\n}\n"],"mappings":"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","names":[]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { execSync } from "child_process";
5
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
6
+ import { join, resolve } from "path";
7
+ function main(argv) {
8
+ const [command, ...rest] = argv;
9
+ if (command !== "stamp") {
10
+ process.stderr.write("usage: subaru stamp [outDir] [--id ID] [--notes TEXT] [--url URL]\n");
11
+ return command === void 0 || command === "--help" || command === "-h" ? 0 : 2;
12
+ }
13
+ let outDir = "public";
14
+ let id;
15
+ let notes;
16
+ let url;
17
+ for (let i = 0; i < rest.length; i++) {
18
+ const arg = rest[i];
19
+ if (arg === "--id") id = rest[++i];
20
+ else if (arg === "--notes") notes = rest[++i];
21
+ else if (arg === "--url") url = rest[++i];
22
+ else outDir = arg;
23
+ }
24
+ const version = packageVersion();
25
+ const commit = gitShort();
26
+ const builtAt = (/* @__PURE__ */ new Date()).toISOString();
27
+ const stamp = {
28
+ id: id ?? [version, commit, Date.now().toString(36)].filter(Boolean).join("."),
29
+ builtAt
30
+ };
31
+ if (version) stamp.version = version;
32
+ if (commit) stamp.commit = commit;
33
+ if (notes) stamp.notes = notes;
34
+ if (url) stamp.url = url;
35
+ const dir = resolve(outDir);
36
+ mkdirSync(dir, { recursive: true });
37
+ const file = join(dir, "subaru.json");
38
+ writeFileSync(file, JSON.stringify(stamp, null, 2) + "\n");
39
+ process.stdout.write(`subaru: wrote ${file} (id ${stamp.id})
40
+ `);
41
+ return 0;
42
+ }
43
+ function packageVersion() {
44
+ try {
45
+ const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
46
+ return pkg.version ?? "";
47
+ } catch {
48
+ return "";
49
+ }
50
+ }
51
+ function gitShort() {
52
+ try {
53
+ return execSync("git rev-parse --short HEAD", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
54
+ } catch {
55
+ return "";
56
+ }
57
+ }
58
+ process.exit(main(process.argv.slice(2)));
59
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * subaru stamp [outDir] [--id ID] [--notes TEXT] [--url URL]\n *\n * Writes <outDir>/subaru.json (default public/) describing this build, so a\n * deployed web app can notice the next deploy. Run it in the build script,\n * before the bundler copies the public directory.\n */\nimport { execSync } from 'node:child_process';\nimport { mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\n\nfunction main(argv: string[]): number {\n const [command, ...rest] = argv;\n if (command !== 'stamp') {\n process.stderr.write('usage: subaru stamp [outDir] [--id ID] [--notes TEXT] [--url URL]\\n');\n return command === undefined || command === '--help' || command === '-h' ? 0 : 2;\n }\n let outDir = 'public';\n let id: string | undefined;\n let notes: string | undefined;\n let url: string | undefined;\n for (let i = 0; i < rest.length; i++) {\n const arg = rest[i]!;\n if (arg === '--id') id = rest[++i];\n else if (arg === '--notes') notes = rest[++i];\n else if (arg === '--url') url = rest[++i];\n else outDir = arg;\n }\n const version = packageVersion();\n const commit = gitShort();\n const builtAt = new Date().toISOString();\n const stamp: Record<string, string> = {\n id: id ?? [version, commit, Date.now().toString(36)].filter(Boolean).join('.'),\n builtAt,\n };\n if (version) stamp.version = version;\n if (commit) stamp.commit = commit;\n if (notes) stamp.notes = notes;\n if (url) stamp.url = url;\n const dir = resolve(outDir);\n mkdirSync(dir, { recursive: true });\n const file = join(dir, 'subaru.json');\n writeFileSync(file, JSON.stringify(stamp, null, 2) + '\\n');\n process.stdout.write(`subaru: wrote ${file} (id ${stamp.id})\\n`);\n return 0;\n}\n\nfunction packageVersion(): string {\n try {\n const pkg = JSON.parse(readFileSync(resolve('package.json'), 'utf8')) as { version?: string };\n return pkg.version ?? '';\n } catch {\n return '';\n }\n}\n\nfunction gitShort(): string {\n try {\n return execSync('git rev-parse --short HEAD', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();\n } catch {\n return '';\n }\n}\n\nprocess.exit(main(process.argv.slice(2)));\n"],"mappings":";;;AAQA,SAAS,gBAAgB;AACzB,SAAS,WAAW,cAAc,qBAAqB;AACvD,SAAS,MAAM,eAAe;AAE9B,SAAS,KAAK,MAAwB;AACpC,QAAM,CAAC,SAAS,GAAG,IAAI,IAAI;AAC3B,MAAI,YAAY,SAAS;AACvB,YAAQ,OAAO,MAAM,qEAAqE;AAC1F,WAAO,YAAY,UAAa,YAAY,YAAY,YAAY,OAAO,IAAI;AAAA,EACjF;AACA,MAAI,SAAS;AACb,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,UAAM,MAAM,KAAK,CAAC;AAClB,QAAI,QAAQ,OAAQ,MAAK,KAAK,EAAE,CAAC;AAAA,aACxB,QAAQ,UAAW,SAAQ,KAAK,EAAE,CAAC;AAAA,aACnC,QAAQ,QAAS,OAAM,KAAK,EAAE,CAAC;AAAA,QACnC,UAAS;AAAA,EAChB;AACA,QAAM,UAAU,eAAe;AAC/B,QAAM,SAAS,SAAS;AACxB,QAAM,WAAU,oBAAI,KAAK,GAAE,YAAY;AACvC,QAAM,QAAgC;AAAA,IACpC,IAAI,MAAM,CAAC,SAAS,QAAQ,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAAA,IAC7E;AAAA,EACF;AACA,MAAI,QAAS,OAAM,UAAU;AAC7B,MAAI,OAAQ,OAAM,SAAS;AAC3B,MAAI,MAAO,OAAM,QAAQ;AACzB,MAAI,IAAK,OAAM,MAAM;AACrB,QAAM,MAAM,QAAQ,MAAM;AAC1B,YAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAClC,QAAM,OAAO,KAAK,KAAK,aAAa;AACpC,gBAAc,MAAM,KAAK,UAAU,OAAO,MAAM,CAAC,IAAI,IAAI;AACzD,UAAQ,OAAO,MAAM,iBAAiB,IAAI,QAAQ,MAAM,EAAE;AAAA,CAAK;AAC/D,SAAO;AACT;AAEA,SAAS,iBAAyB;AAChC,MAAI;AACF,UAAM,MAAM,KAAK,MAAM,aAAa,QAAQ,cAAc,GAAG,MAAM,CAAC;AACpE,WAAO,IAAI,WAAW;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,WAAmB;AAC1B,MAAI;AACF,WAAO,SAAS,8BAA8B,EAAE,OAAO,CAAC,UAAU,QAAQ,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,KAAK;AAAA,EACzG,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,QAAQ,KAAK,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC;","names":[]}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The runtime-independent heart of subaru: the same policy, throttle and
3
+ * memory that the Go module has, driven by an Adapter that knows how a given
4
+ * runtime (a web page, a Tauri window, an Electron main process) finds and
5
+ * applies a release.
6
+ */
7
+ /** What to do when a newer release exists. Identical to the Go module. */
8
+ type Policy = 'prompt' | 'notify' | 'auto' | 'off';
9
+ /** A release newer than the running program. */
10
+ interface Release {
11
+ /** Identity of the release: a tag, a version, or a build id. Skips and installs are remembered by it. */
12
+ tag: string;
13
+ /** Human-readable version when it differs from the tag (web builds). */
14
+ version?: string;
15
+ notes?: string;
16
+ url?: string;
17
+ date?: string;
18
+ }
19
+ type Status = 'idle' | 'checking' | 'current' | 'available' | 'installing' | 'ready' | 'error';
20
+ interface UpdaterState {
21
+ status: Status;
22
+ policy: Policy;
23
+ release: Release | null;
24
+ /** 0..1 while installing when the runtime reports progress. */
25
+ progress: number | null;
26
+ error: string | null;
27
+ /** The release is one the user skipped: mention it, never ask. */
28
+ skipped: boolean;
29
+ /** Hidden for this session by the user. */
30
+ dismissed: boolean;
31
+ /** How the runtime applies an update, for wording. */
32
+ kind: AdapterKind;
33
+ }
34
+ type AdapterKind = 'web' | 'tauri' | 'electron' | 'source' | 'custom';
35
+ interface InstallOptions {
36
+ /** Restart (or reload) as soon as the release is applied. False means "apply now, take effect on the next start". */
37
+ relaunch: boolean;
38
+ onProgress: (fraction: number | null) => void;
39
+ }
40
+ /** What install did: the program is going away, or the update waits for the next start. */
41
+ type InstallOutcome = 'restarting' | 'ready';
42
+ interface Adapter {
43
+ kind: AdapterKind;
44
+ /** Stable name used to key persisted state. */
45
+ name: string;
46
+ /** Resolve a newer release, or null when the program is current. */
47
+ check(signal: AbortSignal): Promise<Release | null>;
48
+ /** Download and apply the release. */
49
+ install(release: Release, options: InstallOptions): Promise<InstallOutcome>;
50
+ /** Restart to finish a 'ready' update. */
51
+ restart?(): Promise<void>;
52
+ /** Suggested minimum time between checks, in milliseconds. */
53
+ interval?: number;
54
+ /** Where to remember the next check time and answered releases. */
55
+ storage?: StateStorage;
56
+ }
57
+ /** The persistence subaru needs: two string operations, sync or async. */
58
+ interface StateStorage {
59
+ get(key: string): string | null | Promise<string | null>;
60
+ set(key: string, value: string): void | Promise<void>;
61
+ }
62
+ interface UpdaterOptions {
63
+ adapter: Adapter;
64
+ policy?: Policy;
65
+ /** Minimum time between checks, in milliseconds. Defaults to the adapter's suggestion, then 24 hours. */
66
+ interval?: number;
67
+ /** Abort a check after this long. Default 15 seconds. */
68
+ timeout?: number;
69
+ storage?: StateStorage;
70
+ /** Diagnostics sink. */
71
+ debug?: (message: string) => void;
72
+ /** Injection points for tests. */
73
+ now?: () => number;
74
+ schedule?: (fn: () => void, ms: number) => () => void;
75
+ }
76
+ /** The surface a UI talks to. Both the core Updater and the Electron renderer proxy provide it. */
77
+ interface UpdaterLike {
78
+ getState(): UpdaterState;
79
+ subscribe(listener: (state: UpdaterState) => void): () => void;
80
+ /** Force a check now, ignoring the interval. */
81
+ check(): Promise<void>;
82
+ /** Install the known release and restart. */
83
+ install(): Promise<void>;
84
+ /** Restart to finish a 'ready' update. */
85
+ restart(): Promise<void>;
86
+ /** Never ask about this release again. */
87
+ skip(): Promise<void>;
88
+ /** Hide the notice for this session. */
89
+ dismiss(): void;
90
+ }
91
+ interface Updater extends UpdaterLike {
92
+ /** Begin background checks according to the policy. Safe to call once. */
93
+ start(): void;
94
+ stop(): void;
95
+ }
96
+ declare function createUpdater(options: UpdaterOptions): Updater;
97
+ /** State that lives only as long as the process. */
98
+ declare function memoryStorage(): StateStorage;
99
+ /** Browser storage that tolerates private mode and quota errors. */
100
+ declare function webStorage(area?: Storage): StateStorage;
101
+ /** Policy from a string, for environment variables and query flags. */
102
+ declare function parsePolicy(value: string | undefined | null): Policy | null;
103
+
104
+ export { type Adapter as A, type InstallOptions as I, type Policy as P, type Release as R, type StateStorage as S, type UpdaterState as U, type UpdaterLike as a, type Updater as b, type AdapterKind as c, type InstallOutcome as d, type Status as e, type UpdaterOptions as f, createUpdater as g, memoryStorage as m, parsePolicy as p, webStorage as w };