offsite 0.0.3 → 0.0.4

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.
Files changed (114) hide show
  1. package/README.md +18 -7
  2. package/apps/server/dist/main.mjs +5767 -0
  3. package/apps/server/dist/main.mjs.map +1 -0
  4. package/apps/web/dist/assets/{angular-html-ji4UO7hp.js → angular-html-prQpjz7s.js} +1 -1
  5. package/apps/web/dist/assets/{angular-ts-CkobLhDa.js → angular-ts-BE4HJ5Uh.js} +1 -1
  6. package/apps/web/dist/assets/{apl-DAjPJTs4.js → apl-CJ-NIQ-7.js} +1 -1
  7. package/apps/web/dist/assets/{astro-DlwA-zmc.js → astro-CVkyXRVY.js} +1 -1
  8. package/apps/web/dist/assets/{blade-wTlHdbgB.js → blade-CQFMHse_.js} +1 -1
  9. package/apps/web/dist/assets/{c-HyCIpwKL.js → c-gqNWYRL8.js} +1 -1
  10. package/apps/web/dist/assets/{chapel-SOYLpFSa.js → chapel-CQ9_BV4j.js} +1 -1
  11. package/apps/web/dist/assets/{cobol-DlvLD04V.js → cobol-D-9v184m.js} +1 -1
  12. package/apps/web/dist/assets/{coffee-CGp5oJTY.js → coffee-BE0tIOLL.js} +1 -1
  13. package/apps/web/dist/assets/{cpp-CzmH13Nm.js → cpp-CzOLC-NF.js} +1 -1
  14. package/apps/web/dist/assets/{crystal-zS14MbR_.js → crystal-f1GcBNi_.js} +1 -1
  15. package/apps/web/dist/assets/{css-DQEQn59R.js → css-DlqRAumc.js} +1 -1
  16. package/apps/web/dist/assets/{edge-Bkv0KzZU.js → edge-DQkkPJaW.js} +1 -1
  17. package/apps/web/dist/assets/{elixir-B8tse0js.js → elixir-ClG78o74.js} +1 -1
  18. package/apps/web/dist/assets/{elm-F2InAxd7.js → elm-DZDmCmuw.js} +1 -1
  19. package/apps/web/dist/assets/{erb-CAIamwOm.js → erb-B1mu0-DN.js} +1 -1
  20. package/apps/web/dist/assets/{git-rebase-U0z47rUi.js → git-rebase-x1gs-rK2.js} +1 -1
  21. package/apps/web/dist/assets/{glimmer-js-Bg_zPoN-.js → glimmer-js-Cjos1igW.js} +1 -1
  22. package/apps/web/dist/assets/{glimmer-ts-v3suqAPH.js → glimmer-ts-CrR-PFvn.js} +1 -1
  23. package/apps/web/dist/assets/{glsl-8io-Pcmd.js → glsl-DR0G8e99.js} +1 -1
  24. package/apps/web/dist/assets/{graphql-B_q9cV-C.js → graphql-DKmrBZt7.js} +1 -1
  25. package/apps/web/dist/assets/{hack-DosfNWDX.js → hack-Brs_j9Xv.js} +1 -1
  26. package/apps/web/dist/assets/{haml-DcBtkGn5.js → haml-8Lc-lF96.js} +1 -1
  27. package/apps/web/dist/assets/{handlebars-2F0KxPFu.js → handlebars-Ds8WdPdv.js} +1 -1
  28. package/apps/web/dist/assets/{html-DrWZZyp3.js → html-C0gvYDho.js} +1 -1
  29. package/apps/web/dist/assets/{html-derivative-BaV8vDp-.js → html-derivative-CU9dkErg.js} +1 -1
  30. package/apps/web/dist/assets/{http-Dwo7FxJA.js → http-BE0w0Fty.js} +1 -1
  31. package/apps/web/dist/assets/{hurl-9r6SjmMP.js → hurl-DqZk6YcW.js} +1 -1
  32. package/apps/web/dist/assets/index-0B-Gdd7b.css +2 -0
  33. package/apps/web/dist/assets/index-rgKS_aMw.js +1148 -0
  34. package/apps/web/dist/assets/{java-CAkt7S2h.js → java-PSSQ0YmM.js} +1 -1
  35. package/apps/web/dist/assets/{javascript-CMo8GEjU.js → javascript-Ci3r4rzs.js} +1 -1
  36. package/apps/web/dist/assets/{jinja-XhRt7Tzv.js → jinja-lLTko9Wl.js} +1 -1
  37. package/apps/web/dist/assets/{jison-BXZQEb4C.js → jison-1BpHD5UK.js} +1 -1
  38. package/apps/web/dist/assets/{json-SyJDQmRp.js → json-BAUn76M4.js} +1 -1
  39. package/apps/web/dist/assets/{jsx-B76-zKzH.js → jsx-B8sFp1vz.js} +1 -1
  40. package/apps/web/dist/assets/{julia-Ca5lbuMA.js → julia-C4TZ4EL9.js} +1 -1
  41. package/apps/web/dist/assets/{just-CDt2RojJ.js → just-D5qIrqkp.js} +1 -1
  42. package/apps/web/dist/assets/{latex-D9bybvWo.js → latex-BL7XYxFg.js} +1 -1
  43. package/apps/web/dist/assets/{liquid-DMp-tByG.js → liquid-DBx9Hq7B.js} +1 -1
  44. package/apps/web/dist/assets/{lua-zNnfh_4o.js → lua-BqjicQyd.js} +1 -1
  45. package/apps/web/dist/assets/{marko-Ce5B11L0.js → marko-CHgy7yaY.js} +1 -1
  46. package/apps/web/dist/assets/{mdc-Btapt9E8.js → mdc-DOloBwb8.js} +1 -1
  47. package/apps/web/dist/assets/{nginx-Bzjverzh.js → nginx-g7I1HPwV.js} +1 -1
  48. package/apps/web/dist/assets/{nim-D6zpNhUX.js → nim-O6zJV1LD.js} +1 -1
  49. package/apps/web/dist/assets/{org-DTy-FvFD.js → org-HLg4-757.js} +1 -1
  50. package/apps/web/dist/assets/{perl-BV8Dxusy.js → perl-B3cDKTai.js} +1 -1
  51. package/apps/web/dist/assets/{php-BjkIa26S.js → php-2eDApoU3.js} +1 -1
  52. package/apps/web/dist/assets/{pug-DdfmTHk6.js → pug-itzGn-nK.js} +1 -1
  53. package/apps/web/dist/assets/{qml-BWLYImhB.js → qml-sU0VtUmf.js} +1 -1
  54. package/apps/web/dist/assets/{r-CKc9klse.js → r-C6mhPqgr.js} +1 -1
  55. package/apps/web/dist/assets/{razor-YcF2a59Y.js → razor-CSzRtbW7.js} +1 -1
  56. package/apps/web/dist/assets/{regexp-D3esVJ1X.js → regexp-BmdBJRV9.js} +1 -1
  57. package/apps/web/dist/assets/{rst-DVaj4AQD.js → rst-BRxUd6sp.js} +1 -1
  58. package/apps/web/dist/assets/{ruby-DBJQEVev.js → ruby-CQHHOfmX.js} +1 -1
  59. package/apps/web/dist/assets/{sas-NSuLkGD1.js → sas-DlaL8b25.js} +1 -1
  60. package/apps/web/dist/assets/{scss-TMy8m-so.js → scss-Cc71ABWp.js} +1 -1
  61. package/apps/web/dist/assets/{shellscript-BVuIsFkY.js → shellscript-CzA_LIIm.js} +1 -1
  62. package/apps/web/dist/assets/{shellsession-CV7MHIu6.js → shellsession-DESJlgp3.js} +1 -1
  63. package/apps/web/dist/assets/{soy-DDY0EoKE.js → soy-Nkvq88YN.js} +1 -1
  64. package/apps/web/dist/assets/{sql-CoZqqtiv.js → sql-CtPxrO7T.js} +1 -1
  65. package/apps/web/dist/assets/{stata-B8C1noIb.js → stata-BacEfVTr.js} +1 -1
  66. package/apps/web/dist/assets/{surrealql-BYiUJt-V.js → surrealql-CYW10JNe.js} +1 -1
  67. package/apps/web/dist/assets/{svelte-DGeZ8dPs.js → svelte-D3_tCKGs.js} +1 -1
  68. package/apps/web/dist/assets/{templ-Dn21SE_C.js → templ-BGKIfY_b.js} +1 -1
  69. package/apps/web/dist/assets/{tex-Da9GBCsM.js → tex-BkkYOaXX.js} +1 -1
  70. package/apps/web/dist/assets/{ts-tags-BuL1T6K3.js → ts-tags-B02tnbpZ.js} +1 -1
  71. package/apps/web/dist/assets/{tsx-DiLCnfgl.js → tsx-D3MB1IUf.js} +1 -1
  72. package/apps/web/dist/assets/{twig-Cx1peiH-.js → twig-CPhw2wGK.js} +1 -1
  73. package/apps/web/dist/assets/{typescript-pVxvHJ4R.js → typescript-B7U4faGm.js} +1 -1
  74. package/apps/web/dist/assets/{typst-CJQbitzj.js → typst-CN9EZswy.js} +1 -1
  75. package/apps/web/dist/assets/{vue-CXKyVnbB.js → vue-YH8dfjo_.js} +1 -1
  76. package/apps/web/dist/assets/{vue-html-AQjnBMTh.js → vue-html-NwX2xzgp.js} +1 -1
  77. package/apps/web/dist/assets/{vue-vine-DYxt9AbK.js → vue-vine-DoRmnjLj.js} +1 -1
  78. package/apps/web/dist/assets/{xml-CEJ01uJ1.js → xml-DgfgLpPc.js} +1 -1
  79. package/apps/web/dist/assets/{xsl-CULFBy6v.js → xsl-CEwQRUK_.js} +1 -1
  80. package/apps/web/dist/assets/{yaml-BNf68xM_.js → yaml-CEFOzF95.js} +1 -1
  81. package/apps/web/dist/icon.svg +5 -0
  82. package/apps/web/dist/index.html +2 -2
  83. package/apps/web/dist/sw.js +19 -8
  84. package/bin/offsite.js +1 -1
  85. package/package.json +5 -1
  86. package/apps/server/dist/args.js +0 -11
  87. package/apps/server/dist/claude.js +0 -245
  88. package/apps/server/dist/config.js +0 -191
  89. package/apps/server/dist/content.js +0 -172
  90. package/apps/server/dist/db.js +0 -13
  91. package/apps/server/dist/files.js +0 -50
  92. package/apps/server/dist/git.js +0 -133
  93. package/apps/server/dist/history.js +0 -137
  94. package/apps/server/dist/hub.js +0 -10
  95. package/apps/server/dist/main.js +0 -70
  96. package/apps/server/dist/projects.js +0 -92
  97. package/apps/server/dist/protocol.js +0 -20
  98. package/apps/server/dist/push.js +0 -152
  99. package/apps/server/dist/run.js +0 -375
  100. package/apps/server/dist/runs.js +0 -102
  101. package/apps/server/dist/schemes.js +0 -262
  102. package/apps/server/dist/sessions.js +0 -99
  103. package/apps/server/dist/socket.js +0 -193
  104. package/apps/server/dist/storage.js +0 -71
  105. package/apps/server/dist/terminal/alerts.js +0 -145
  106. package/apps/server/dist/terminal/process.js +0 -207
  107. package/apps/server/dist/terminal/shell-check.js +0 -91
  108. package/apps/server/dist/terminal/shell-integration.js +0 -112
  109. package/apps/server/dist/terminal/terminal.js +0 -402
  110. package/apps/web/dist/assets/index-CIBt6t2M.css +0 -2
  111. package/apps/web/dist/assets/index-CPpMMYyZ.js +0 -902
  112. /package/apps/server/src/{terminal → services/terminal}/shell-integration/bash/offsite.bash +0 -0
  113. /package/apps/server/src/{terminal → services/terminal}/shell-integration/fish/offsite.fish +0 -0
  114. /package/apps/server/src/{terminal → services/terminal}/shell-integration/zsh/.zshenv +0 -0
@@ -1,10 +0,0 @@
1
- export const clients = new Set();
2
- export function publish(sessionId, event) {
3
- for (const client of clients)
4
- if (client.sessionId === sessionId)
5
- client.send(event);
6
- }
7
- export function broadcast(event) {
8
- for (const client of clients)
9
- client.send(event);
10
- }
@@ -1,70 +0,0 @@
1
- import { createReadStream, existsSync, statSync } from 'node:fs';
2
- import { createServer } from 'node:http';
3
- import path from 'node:path';
4
- import { WebSocketServer } from 'ws';
5
- import { args } from './args.js';
6
- import { findClaude } from './claude.js';
7
- import { readConfig } from './config.js';
8
- import { startPush } from './push.js';
9
- import { stopAll } from './runs.js';
10
- import { watchSessions } from './sessions.js';
11
- import { connectClient } from './socket.js';
12
- const contentTypes = {
13
- '.html': 'text/html; charset=utf-8',
14
- '.js': 'text/javascript; charset=utf-8',
15
- '.css': 'text/css; charset=utf-8',
16
- '.svg': 'image/svg+xml',
17
- '.png': 'image/png',
18
- '.ico': 'image/x-icon',
19
- '.json': 'application/json',
20
- '.webmanifest': 'application/manifest+json',
21
- '.woff2': 'font/woff2',
22
- };
23
- const port = Number(args.port);
24
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
25
- console.error(`invalid port ${args.port}`);
26
- process.exit(1);
27
- }
28
- process.env.CLAUDE_CONFIG_DIR = readConfig().claudeBaseDir;
29
- watchSessions();
30
- if (findClaude() === null) {
31
- console.error('could not find claude on PATH; install Claude Code first');
32
- process.exit(1);
33
- }
34
- const publicDir = path.join(import.meta.dirname, '..', '..', 'web', 'dist');
35
- const server = createServer((request, response) => {
36
- const url = new URL(request.url ?? '/', 'http://localhost');
37
- serveFile(url.pathname, response);
38
- });
39
- const sockets = new WebSocketServer({ noServer: true });
40
- server.on('upgrade', (request, socket, head) => {
41
- const url = new URL(request.url ?? '/', 'http://localhost');
42
- if (url.pathname === '/ws')
43
- sockets.handleUpgrade(request, socket, head, connectClient);
44
- else
45
- socket.destroy();
46
- });
47
- server.listen(port, args.host, () => {
48
- startPush(`${args.host}:${port}`);
49
- console.log(`offsite is running at http://${args.host}:${port}`);
50
- });
51
- process.on('SIGINT', stop);
52
- process.on('SIGTERM', stop);
53
- function stop() {
54
- stopAll();
55
- process.exit(0);
56
- }
57
- function serveFile(pathname, response) {
58
- const requested = path.join(publicDir, path.normalize(pathname));
59
- const file = requested.startsWith(publicDir + path.sep) &&
60
- statSync(requested, { throwIfNoEntry: false })?.isFile()
61
- ? requested
62
- : path.join(publicDir, 'index.html');
63
- if (!existsSync(file)) {
64
- response.statusCode = 404;
65
- response.end('web app not built; run pnpm build');
66
- return;
67
- }
68
- response.setHeader('content-type', contentTypes[path.extname(file)] ?? 'application/octet-stream');
69
- createReadStream(file).pipe(response);
70
- }
@@ -1,92 +0,0 @@
1
- import { readdir, stat } from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { db } from './db.js';
5
- import { broadcast } from './hub.js';
6
- const LOGO_TYPES = [
7
- 'image/png',
8
- 'image/jpeg',
9
- 'image/gif',
10
- 'image/webp',
11
- 'image/svg+xml',
12
- 'image/x-icon',
13
- 'image/vnd.microsoft.icon',
14
- ];
15
- const LOGO_LIMIT = 512 * 1024;
16
- export function projects() {
17
- return db
18
- .prepare('SELECT path, name, logo FROM projects')
19
- .all()
20
- .map(toProject);
21
- }
22
- export async function addProject(input) {
23
- const folder = path.resolve(expandHome(input.trim()));
24
- if (!(await stat(folder)).isDirectory())
25
- throw new Error(`${folder} is not a folder`);
26
- db.prepare('INSERT OR IGNORE INTO projects (path, name, logo) VALUES (?, ?, NULL)').run(folder, path.basename(folder) || folder);
27
- publish();
28
- const project = projects().find((item) => item.path === folder);
29
- if (project === undefined)
30
- throw new Error(`Could not add ${folder}`);
31
- return project;
32
- }
33
- export function updateProject(folder, patch) {
34
- if (patch.name !== undefined) {
35
- const name = patch.name.trim();
36
- if (name === '')
37
- throw new Error('Project name cannot be empty');
38
- db.prepare('UPDATE projects SET name = ? WHERE path = ?').run(name, folder);
39
- }
40
- if (patch.logo !== undefined) {
41
- if (patch.logo !== null)
42
- checkLogo(patch.logo);
43
- db.prepare('UPDATE projects SET logo = ? WHERE path = ?').run(patch.logo, folder);
44
- }
45
- publish();
46
- return null;
47
- }
48
- export function removeProject(folder) {
49
- db.prepare('DELETE FROM projects WHERE path = ?').run(folder);
50
- publish();
51
- return null;
52
- }
53
- export async function directories(input) {
54
- const folder = path.resolve(expandHome(input.trim()));
55
- const entries = await readdir(folder, { withFileTypes: true });
56
- const parent = path.dirname(folder);
57
- return {
58
- path: folder,
59
- parent: parent === folder ? null : parent,
60
- directories: entries
61
- .filter((entry) => entry.isDirectory())
62
- .map((entry) => ({
63
- name: entry.name,
64
- path: path.join(folder, entry.name),
65
- }))
66
- .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())),
67
- };
68
- }
69
- function publish() {
70
- broadcast({ type: 'projects', projects: projects() });
71
- }
72
- function toProject(row) {
73
- if (typeof row.path !== 'string' ||
74
- typeof row.name !== 'string' ||
75
- (typeof row.logo !== 'string' && row.logo !== null))
76
- throw new Error('The projects table has an unexpected row');
77
- return { path: row.path, name: row.name, logo: row.logo };
78
- }
79
- function checkLogo(logo) {
80
- const type = /^data:([^;,]+);base64,/.exec(logo)?.[1];
81
- if (type === undefined || !LOGO_TYPES.includes(type))
82
- throw new Error('Use a PNG, JPEG, GIF, WebP, SVG or ICO image');
83
- if (logo.length > LOGO_LIMIT)
84
- throw new Error('Use an image smaller than 384 KB');
85
- }
86
- function expandHome(input) {
87
- if (input === '~')
88
- return os.homedir();
89
- if (input.startsWith('~/'))
90
- return path.join(os.homedir(), input.slice(2));
91
- return input;
92
- }
@@ -1,20 +0,0 @@
1
- export const FRAME_INPUT = 0;
2
- export const FRAME_REPLY = 1;
3
- export const FRAME_OUTPUT = 2;
4
- export const FRAME_REPLAY = 3;
5
- export function encodeFrame(kind, id, data) {
6
- const frame = new Uint8Array(5 + data.length);
7
- frame[0] = kind;
8
- new DataView(frame.buffer).setUint32(1, id);
9
- frame.set(data, 5);
10
- return frame;
11
- }
12
- export function decodeFrame(frame) {
13
- if (frame.length < 5)
14
- throw new Error('terminal frame is too short');
15
- return {
16
- kind: frame[0],
17
- id: new DataView(frame.buffer, frame.byteOffset, frame.byteLength).getUint32(1),
18
- data: frame.subarray(5),
19
- };
20
- }
@@ -1,152 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { renameSync } from 'node:fs';
3
- import path from 'node:path';
4
- import webPush from 'web-push';
5
- import { home, readJson, writeJson } from './storage.js';
6
- const pushFile = path.join(home, 'push.json');
7
- const TITLE_LIMIT = 100;
8
- const BODY_LIMIT = 500;
9
- const TIME_TO_LIVE = 15 * 60;
10
- const SEND_TIMEOUT = 10_000;
11
- const SUBJECT = 'https://github.com/NazmusSayad/offsite';
12
- let server = '';
13
- let active = false;
14
- export function startPush(address) {
15
- server = address;
16
- try {
17
- const store = readJson('push.json');
18
- if (store !== null)
19
- track(store);
20
- }
21
- catch (error) {
22
- console.warn(`push notifications are off until a browser turns them on again, could not read ${pushFile}: ${error}`);
23
- }
24
- }
25
- export function pushActive() {
26
- return active;
27
- }
28
- export function pushKey() {
29
- return { publicKey: read().publicKey };
30
- }
31
- export function subscribe(subscription) {
32
- const endpoint = new URL(subscription.endpoint);
33
- if (endpoint.protocol !== 'https:')
34
- throw new Error('the endpoint must be an https URL');
35
- update((store) => {
36
- store.subscriptions = store.subscriptions.filter((existing) => existing.endpoint !== subscription.endpoint);
37
- store.subscriptions.push({
38
- endpoint: subscription.endpoint,
39
- keys: { p256dh: subscription.keys.p256dh, auth: subscription.keys.auth },
40
- server,
41
- });
42
- });
43
- console.log('a browser subscribed to push notifications');
44
- }
45
- export function unsubscribe(endpoint) {
46
- update((store) => {
47
- store.subscriptions = store.subscriptions.filter((existing) => existing.endpoint !== endpoint);
48
- });
49
- console.log('a browser unsubscribed from push notifications');
50
- }
51
- export async function sendPush(message) {
52
- let store;
53
- try {
54
- store = read();
55
- }
56
- catch (error) {
57
- console.error(`could not read push subscriptions: ${error}`);
58
- return;
59
- }
60
- const subscriptions = store.subscriptions.filter(ours);
61
- if (subscriptions.length === 0)
62
- return;
63
- const payload = JSON.stringify({
64
- ...message,
65
- title: [...message.title].slice(0, TITLE_LIMIT).join(''),
66
- body: [...message.body].slice(0, BODY_LIMIT).join(''),
67
- });
68
- const gone = [];
69
- await Promise.all(subscriptions.map(async (subscription) => {
70
- try {
71
- await webPush.sendNotification(subscription, payload, {
72
- vapidDetails: {
73
- subject: SUBJECT,
74
- publicKey: store.publicKey,
75
- privateKey: store.privateKey,
76
- },
77
- TTL: TIME_TO_LIVE,
78
- urgency: 'high',
79
- timeout: SEND_TIMEOUT,
80
- });
81
- }
82
- catch (error) {
83
- const status = error.statusCode;
84
- if (status !== undefined && [401, 403, 404, 410].includes(status)) {
85
- console.log(`push subscription ${subscription.endpoint} is gone or was made for another key, removing it`);
86
- gone.push(subscription.endpoint);
87
- return;
88
- }
89
- console.warn(`could not send a push to ${subscription.endpoint}: ${error}`);
90
- }
91
- }));
92
- if (gone.length === 0)
93
- return;
94
- try {
95
- update((current) => {
96
- current.subscriptions = current.subscriptions.filter((subscription) => !gone.includes(subscription.endpoint));
97
- });
98
- }
99
- catch (error) {
100
- console.error(`could not remove gone push subscriptions: ${error}`);
101
- }
102
- }
103
- function ours(subscription) {
104
- return subscription.server === server;
105
- }
106
- function track(store) {
107
- active = store.subscriptions.some(ours);
108
- }
109
- function load() {
110
- let store;
111
- try {
112
- store = readJson('push.json');
113
- }
114
- catch (error) {
115
- if (!(error instanceof SyntaxError))
116
- throw error;
117
- return replaceCorrupt(error);
118
- }
119
- if (store === null)
120
- return create();
121
- if (typeof store.publicKey !== 'string' ||
122
- typeof store.privateKey !== 'string' ||
123
- !Array.isArray(store.subscriptions))
124
- return replaceCorrupt(new Error('it is missing its keys or subscriptions'));
125
- return store;
126
- }
127
- function replaceCorrupt(error) {
128
- const aside = `${pushFile}.corrupt-${randomUUID()}`;
129
- renameSync(pushFile, aside);
130
- console.warn(`${pushFile} is corrupt, moved it to ${aside} and created a new one: ${error}`);
131
- return create();
132
- }
133
- function create() {
134
- const keys = webPush.generateVAPIDKeys();
135
- const store = { ...keys, subscriptions: [] };
136
- save(store);
137
- return store;
138
- }
139
- function save(store) {
140
- writeJson('push.json', store);
141
- }
142
- function read() {
143
- const store = load();
144
- track(store);
145
- return store;
146
- }
147
- function update(change) {
148
- const store = load();
149
- change(store);
150
- save(store);
151
- track(store);
152
- }