@happier-dev/stack 0.1.0-preview.17.1 → 0.1.0-preview.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/server-flavors.md +6 -6
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts +2 -0
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -1
- package/node_modules/@happier-dev/cli-common/dist/index.js +2 -0
- package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -1
- package/node_modules/@happier-dev/cli-common/dist/providers/index.d.ts +51 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.js +129 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.d.ts +5 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.js +5 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.d.ts +19 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.js +117 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.d.ts +55 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.js +302 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.d.ts +12 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.js +75 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.d.ts +8 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.js +29 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/package.json +11 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.d.ts +22 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.js +44 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.d.ts +5 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.js +21 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.d.ts +14 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.js +39 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.d.ts +20 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.js +60 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.d.ts +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.js +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.d.ts +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.js +92 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.d.ts +26 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.js +53 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/package.json +38 -0
- package/package.json +4 -2
- package/scripts/auth.mjs +3 -2
- package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +1 -0
- package/scripts/auth_copy_from_runCapture.integration.test.mjs +8 -1
- package/scripts/auth_login_guided_server_no_expo.test.mjs +2 -0
- package/scripts/build.mjs +3 -18
- package/scripts/bundleWorkspaceDeps.mjs +5 -1
- package/scripts/bundleWorkspaceDeps.test.mjs +42 -1
- package/scripts/mobile.mjs +30 -2
- package/scripts/mobile_dev_client.mjs +7 -32
- package/scripts/mobile_dev_client_help_smoke.test.mjs +24 -0
- package/scripts/mobile_prebuild_happyDir_defined.test.mjs +47 -0
- package/scripts/mobile_prebuild_sets_rct_metro_port.test.mjs +81 -0
- package/scripts/mobile_run_ios_passes_port.integration.test.mjs +103 -0
- package/scripts/mobile_run_ios_uses_long_port_flag.test.mjs +106 -0
- package/scripts/providers_cmd.mjs +262 -0
- package/scripts/release_binary_smoke.integration.test.mjs +45 -37
- package/scripts/remote_cmd.mjs +352 -0
- package/scripts/self_host_daemon.real.integration.test.mjs +296 -0
- package/scripts/self_host_launchd.real.integration.test.mjs +211 -0
- package/scripts/self_host_runtime.mjs +1829 -327
- package/scripts/self_host_runtime.test.mjs +523 -1
- package/scripts/self_host_schtasks.real.integration.test.mjs +217 -0
- package/scripts/self_host_service_e2e_harness.mjs +93 -0
- package/scripts/self_host_systemd.real.integration.test.mjs +8 -86
- package/scripts/service.mjs +156 -26
- package/scripts/stack/command_arguments.mjs +1 -0
- package/scripts/stack/help_text.mjs +3 -1
- package/scripts/stack.mjs +2 -1
- package/scripts/stack_daemon_cmd.integration.test.mjs +37 -0
- package/scripts/stack_happy_cmd.integration.test.mjs +36 -0
- package/scripts/stack_pr_help_cmd.test.mjs +38 -0
- package/scripts/stop.mjs +2 -3
- package/scripts/utils/auth/credentials_paths.mjs +9 -9
- package/scripts/utils/auth/credentials_paths.test.mjs +8 -0
- package/scripts/utils/auth/orchestrated_stack_auth_flow.mjs +64 -3
- package/scripts/utils/auth/stable_scope_id.mjs +1 -1
- package/scripts/utils/cli/cli_registry.mjs +21 -0
- package/scripts/utils/cli/progress.mjs +8 -1
- package/scripts/utils/cli/progress.test.mjs +43 -0
- package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +17 -0
- package/scripts/utils/dev/expo_dev.mjs +35 -5
- package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +180 -1
- package/scripts/utils/dev/expo_dev_runtime_metadata.test.mjs +126 -0
- package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +9 -2
- package/scripts/utils/mobile/dev_client_install_invocation.mjs +68 -0
- package/scripts/utils/mobile/dev_client_install_invocation.test.mjs +27 -0
- package/scripts/utils/server/port.mjs +20 -2
- package/scripts/utils/service/service_manager.definition.test.mjs +66 -0
- package/scripts/utils/service/service_manager.mjs +96 -0
- package/scripts/utils/service/service_manager.plan.test.mjs +37 -0
- package/scripts/utils/service/service_manager.test.mjs +20 -0
- package/scripts/utils/service/systemd_service_unit.mjs +1 -0
- package/scripts/utils/service/systemd_service_unit.test.mjs +42 -0
- package/scripts/utils/service/windows_schtasks_wrapper.mjs +1 -0
- package/scripts/utils/service/windows_schtasks_wrapper.test.mjs +25 -0
- package/scripts/utils/ui/ui_export_env.mjs +29 -0
- package/scripts/utils/ui/ui_export_env.test.mjs +25 -0
- package/scripts/worktrees.mjs +3 -0
- package/scripts/worktrees_status_default_target.test.mjs +56 -0
|
@@ -1,13 +1,76 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash, generateKeyPairSync, sign } from 'node:crypto';
|
|
3
|
+
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
|
|
2
4
|
import test from 'node:test';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
3
8
|
|
|
4
9
|
import {
|
|
5
10
|
parseSelfHostInvocation,
|
|
6
11
|
pickReleaseAsset,
|
|
12
|
+
resolveMinisignPublicKeyText,
|
|
13
|
+
resolveSelfHostAutoUpdateDefault,
|
|
14
|
+
resolveSelfHostAutoUpdateIntervalMinutes,
|
|
15
|
+
resolveSelfHostHealthTimeoutMs,
|
|
16
|
+
resolveSelfHostDefaults,
|
|
17
|
+
renderUpdaterLaunchdPlistXml,
|
|
18
|
+
renderUpdaterScheduledTaskWrapperPs1,
|
|
19
|
+
renderUpdaterSystemdUnit,
|
|
20
|
+
renderUpdaterSystemdTimerUnit,
|
|
21
|
+
buildUpdaterScheduledTaskCreateArgs,
|
|
7
22
|
renderServerEnvFile,
|
|
8
23
|
renderServerServiceUnit,
|
|
24
|
+
renderSelfHostStatusText,
|
|
25
|
+
buildSelfHostDoctorChecks,
|
|
26
|
+
normalizeSelfHostAutoUpdateState,
|
|
27
|
+
decideSelfHostAutoUpdateReconcile,
|
|
28
|
+
mergeEnvTextWithDefaults,
|
|
9
29
|
} from './self_host_runtime.mjs';
|
|
10
30
|
|
|
31
|
+
function b64(buf) {
|
|
32
|
+
return Buffer.from(buf).toString('base64');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function base64UrlToBuffer(value) {
|
|
36
|
+
const s = String(value ?? '')
|
|
37
|
+
.replace(/-/g, '+')
|
|
38
|
+
.replace(/_/g, '/')
|
|
39
|
+
.padEnd(Math.ceil(String(value ?? '').length / 4) * 4, '=');
|
|
40
|
+
return Buffer.from(s, 'base64');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createMinisignKeyPair() {
|
|
44
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
45
|
+
const jwk = publicKey.export({ format: 'jwk' });
|
|
46
|
+
const rawPublicKey = base64UrlToBuffer(jwk.x);
|
|
47
|
+
assert.equal(rawPublicKey.length, 32);
|
|
48
|
+
|
|
49
|
+
const keyId = Buffer.from('0123456789abcdef', 'hex');
|
|
50
|
+
const publicKeyBytes = Buffer.concat([Buffer.from('Ed'), keyId, rawPublicKey]);
|
|
51
|
+
const pubkeyFile = `untrusted comment: minisign public key\n${b64(publicKeyBytes)}\n`;
|
|
52
|
+
return { pubkeyFile, keyId, privateKey };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function signMinisignMessage({ message, keyId, privateKey }) {
|
|
56
|
+
const signature = sign(null, message, privateKey);
|
|
57
|
+
const sigLineBytes = Buffer.concat([Buffer.from('Ed'), keyId, signature]);
|
|
58
|
+
const trustedComment = 'trusted comment: test';
|
|
59
|
+
const trustedSuffix = Buffer.from(trustedComment.slice('trusted comment: '.length), 'utf-8');
|
|
60
|
+
const globalSignature = sign(null, Buffer.concat([signature, trustedSuffix]), privateKey);
|
|
61
|
+
return [
|
|
62
|
+
'untrusted comment: signature from happier stack test',
|
|
63
|
+
b64(sigLineBytes),
|
|
64
|
+
trustedComment,
|
|
65
|
+
b64(globalSignature),
|
|
66
|
+
'',
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sha256Hex(bytes) {
|
|
71
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
72
|
+
}
|
|
73
|
+
|
|
11
74
|
test('parseSelfHostInvocation accepts optional self-host prefix', () => {
|
|
12
75
|
const parsed = parseSelfHostInvocation(['self-host', 'install', '--channel=preview']);
|
|
13
76
|
assert.equal(parsed.subcommand, 'install');
|
|
@@ -37,6 +100,81 @@ test('pickReleaseAsset returns matching archive and checksum assets', () => {
|
|
|
37
100
|
assert.equal(picked.signatureUrl, 'https://example.test/checksums.txt.minisig');
|
|
38
101
|
});
|
|
39
102
|
|
|
103
|
+
test('self-host release installer reports archive source url', async (t) => {
|
|
104
|
+
if (process.platform === 'win32') {
|
|
105
|
+
t.skip('tar-based bundle test does not run on windows');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (spawnSync('bash', ['-lc', 'command -v tar >/dev/null 2>&1'], { stdio: 'ignore' }).status !== 0) {
|
|
109
|
+
t.skip('tar is required for bundle installation test');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const tmp = await mkdtemp(join(tmpdir(), 'happier-self-host-bundle-test-'));
|
|
114
|
+
t.after(async () => {
|
|
115
|
+
await spawnSync('bash', ['-lc', `rm -rf "${tmp.replaceAll('"', '\\"')}"`], { stdio: 'ignore' });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const staging = join(tmp, 'staging');
|
|
119
|
+
const rootName = 'happier-server-v1.2.3-preview.1-linux-x64';
|
|
120
|
+
const rootDir = join(staging, rootName);
|
|
121
|
+
await mkdir(join(rootDir, 'generated'), { recursive: true });
|
|
122
|
+
await writeFile(join(rootDir, 'generated', 'dummy.txt'), 'ok', 'utf-8');
|
|
123
|
+
|
|
124
|
+
const binaryName = 'happier-server';
|
|
125
|
+
const binaryPath = join(rootDir, binaryName);
|
|
126
|
+
await writeFile(binaryPath, '#!/bin/sh\necho ok\n', 'utf-8');
|
|
127
|
+
spawnSync('bash', ['-lc', `chmod +x "${binaryPath.replaceAll('"', '\\"')}"`], { stdio: 'ignore' });
|
|
128
|
+
|
|
129
|
+
const archiveName = `${rootName}.tar.gz`;
|
|
130
|
+
const archivePath = join(tmp, archiveName);
|
|
131
|
+
const tar = spawnSync('tar', ['-czf', archivePath, '-C', staging, rootName], { encoding: 'utf-8' });
|
|
132
|
+
assert.equal(tar.status, 0, tar.stderr || tar.stdout);
|
|
133
|
+
|
|
134
|
+
const archiveBytes = await (await import('node:fs/promises')).readFile(archivePath);
|
|
135
|
+
const archiveSha = sha256Hex(archiveBytes);
|
|
136
|
+
const checksumsText = `${archiveSha} ${archiveName}\n`;
|
|
137
|
+
const { pubkeyFile, keyId, privateKey } = createMinisignKeyPair();
|
|
138
|
+
const sigFile = signMinisignMessage({
|
|
139
|
+
message: Buffer.from(checksumsText, 'utf-8'),
|
|
140
|
+
keyId,
|
|
141
|
+
privateKey,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const archiveUrl = `data:application/octet-stream;base64,${archiveBytes.toString('base64')}`;
|
|
145
|
+
const checksumsUrl = `data:text/plain,${encodeURIComponent(checksumsText)}`;
|
|
146
|
+
const sigUrl = `data:text/plain,${encodeURIComponent(sigFile)}`;
|
|
147
|
+
|
|
148
|
+
const bundle = {
|
|
149
|
+
version: '1.2.3-preview.1',
|
|
150
|
+
archive: { name: archiveName, url: archiveUrl },
|
|
151
|
+
checksums: { name: `checksums-happier-server-v1.2.3-preview.1.txt`, url: checksumsUrl },
|
|
152
|
+
checksumsSig: { name: `checksums-happier-server-v1.2.3-preview.1.txt.minisig`, url: sigUrl },
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const installRoot = join(tmp, 'install');
|
|
156
|
+
const config = {
|
|
157
|
+
platform: process.platform,
|
|
158
|
+
dataDir: join(installRoot, 'data'),
|
|
159
|
+
versionsDir: join(installRoot, 'versions'),
|
|
160
|
+
serverBinaryPath: join(installRoot, 'bin', binaryName),
|
|
161
|
+
serverPreviousBinaryPath: join(installRoot, 'bin', `${binaryName}.previous`),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const mod = await import('./self_host_runtime.mjs');
|
|
165
|
+
assert.equal(typeof mod.installSelfHostBinaryFromBundle, 'function');
|
|
166
|
+
|
|
167
|
+
const result = await mod.installSelfHostBinaryFromBundle({
|
|
168
|
+
bundle,
|
|
169
|
+
binaryName,
|
|
170
|
+
config,
|
|
171
|
+
pubkeyFile,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
assert.equal(result.version, '1.2.3-preview.1');
|
|
175
|
+
assert.equal(result.source, archiveUrl);
|
|
176
|
+
});
|
|
177
|
+
|
|
40
178
|
test('pickReleaseAsset rejects releases missing minisign signature assets', () => {
|
|
41
179
|
assert.throws(() => {
|
|
42
180
|
pickReleaseAsset({
|
|
@@ -48,7 +186,37 @@ test('pickReleaseAsset rejects releases missing minisign signature assets', () =
|
|
|
48
186
|
os: 'linux',
|
|
49
187
|
arch: 'x64',
|
|
50
188
|
});
|
|
51
|
-
}, /signature/i);
|
|
189
|
+
}, /minisig|signature/i);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('pickReleaseAsset supports windows zip artifacts', () => {
|
|
193
|
+
const assets = [
|
|
194
|
+
{ name: 'happier-server-v1.2.3-windows-x64.zip', browser_download_url: 'https://example.test/server.zip' },
|
|
195
|
+
{ name: 'checksums-happier-server-v1.2.3.txt', browser_download_url: 'https://example.test/checksums.txt' },
|
|
196
|
+
{ name: 'checksums-happier-server-v1.2.3.txt.minisig', browser_download_url: 'https://example.test/checksums.txt.minisig' },
|
|
197
|
+
];
|
|
198
|
+
const picked = pickReleaseAsset({
|
|
199
|
+
assets,
|
|
200
|
+
product: 'happier-server',
|
|
201
|
+
os: 'windows',
|
|
202
|
+
arch: 'x64',
|
|
203
|
+
});
|
|
204
|
+
assert.equal(picked.archiveUrl, 'https://example.test/server.zip');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('pickReleaseAsset supports windows tar.gz artifacts', () => {
|
|
208
|
+
const assets = [
|
|
209
|
+
{ name: 'happier-server-v1.2.3-windows-x64.tar.gz', browser_download_url: 'https://example.test/server.tgz' },
|
|
210
|
+
{ name: 'checksums-happier-server-v1.2.3.txt', browser_download_url: 'https://example.test/checksums.txt' },
|
|
211
|
+
{ name: 'checksums-happier-server-v1.2.3.txt.minisig', browser_download_url: 'https://example.test/checksums.txt.minisig' },
|
|
212
|
+
];
|
|
213
|
+
const picked = pickReleaseAsset({
|
|
214
|
+
assets,
|
|
215
|
+
product: 'happier-server',
|
|
216
|
+
os: 'windows',
|
|
217
|
+
arch: 'x64',
|
|
218
|
+
});
|
|
219
|
+
assert.equal(picked.archiveUrl, 'https://example.test/server.tgz');
|
|
52
220
|
});
|
|
53
221
|
|
|
54
222
|
test('renderServerServiceUnit references configured binary and env file', () => {
|
|
@@ -65,6 +233,19 @@ test('renderServerServiceUnit references configured binary and env file', () =>
|
|
|
65
233
|
assert.match(unit, /StandardOutput=append:\/var\/log\/happier\/server.log/);
|
|
66
234
|
});
|
|
67
235
|
|
|
236
|
+
test('resolveSelfHostDefaults uses user-mode paths by default', () => {
|
|
237
|
+
const cfg = resolveSelfHostDefaults({ platform: 'linux', mode: 'user', homeDir: '/home/me' });
|
|
238
|
+
assert.equal(cfg.installRoot, '/home/me/.happier/self-host');
|
|
239
|
+
assert.equal(cfg.binDir, '/home/me/.happier/bin');
|
|
240
|
+
assert.equal(cfg.configDir, '/home/me/.happier/self-host/config');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('resolveMinisignPublicKeyText prefers inline override and otherwise returns bundled key', () => {
|
|
244
|
+
const bundled = resolveMinisignPublicKeyText({});
|
|
245
|
+
assert.match(bundled, /minisign public key/i);
|
|
246
|
+
assert.equal(resolveMinisignPublicKeyText({ HAPPIER_MINISIGN_PUBKEY: 'hello' }), 'hello');
|
|
247
|
+
});
|
|
248
|
+
|
|
68
249
|
test('renderServerEnvFile emits sqlite/local defaults for self-host mode', () => {
|
|
69
250
|
const envText = renderServerEnvFile({
|
|
70
251
|
port: 3005,
|
|
@@ -74,9 +255,350 @@ test('renderServerEnvFile emits sqlite/local defaults for self-host mode', () =>
|
|
|
74
255
|
dbDir: '/var/lib/happier/pglite',
|
|
75
256
|
});
|
|
76
257
|
assert.match(envText, /PORT=3005/);
|
|
258
|
+
assert.match(envText, /METRICS_ENABLED=false/);
|
|
77
259
|
assert.match(envText, /HAPPIER_DB_PROVIDER=sqlite/);
|
|
260
|
+
assert.match(envText, /DATABASE_URL=file:\/var\/lib\/happier\/happier-server-light\.sqlite/);
|
|
78
261
|
assert.match(envText, /HAPPIER_FILES_BACKEND=local/);
|
|
262
|
+
assert.match(envText, /HAPPIER_SQLITE_AUTO_MIGRATE=1/);
|
|
263
|
+
assert.match(envText, /HAPPIER_SQLITE_MIGRATIONS_DIR=\/var\/lib\/happier\/migrations\/sqlite/);
|
|
79
264
|
assert.match(envText, /HAPPIER_SERVER_LIGHT_DATA_DIR=\/var\/lib\/happier/);
|
|
80
265
|
assert.match(envText, /HAPPIER_SERVER_LIGHT_FILES_DIR=\/var\/lib\/happier\/files/);
|
|
81
266
|
assert.match(envText, /HAPPIER_SERVER_LIGHT_DB_DIR=\/var\/lib\/happier\/pglite/);
|
|
82
267
|
});
|
|
268
|
+
|
|
269
|
+
test('renderServerEnvFile includes ui bundle directory when provided', () => {
|
|
270
|
+
const envText = renderServerEnvFile({
|
|
271
|
+
port: 3005,
|
|
272
|
+
host: '127.0.0.1',
|
|
273
|
+
dataDir: '/var/lib/happier',
|
|
274
|
+
filesDir: '/var/lib/happier/files',
|
|
275
|
+
dbDir: '/var/lib/happier/pglite',
|
|
276
|
+
uiDir: '/var/lib/happier/ui-web/current',
|
|
277
|
+
});
|
|
278
|
+
assert.match(envText, /HAPPIER_SERVER_UI_DIR=\/var\/lib\/happier\/ui-web\/current/);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('renderServerEnvFile includes PRISMA_QUERY_ENGINE_LIBRARY when a packaged sqlite engine is present', async () => {
|
|
282
|
+
const serverBinDir = await mkdtemp(join(tmpdir(), 'happier-self-host-bin-'));
|
|
283
|
+
await mkdir(join(serverBinDir, 'generated', 'sqlite-client'), { recursive: true });
|
|
284
|
+
const enginePath = join(serverBinDir, 'generated', 'sqlite-client', 'libquery_engine-darwin-arm64.dylib.node');
|
|
285
|
+
await writeFile(enginePath, 'stub', 'utf-8');
|
|
286
|
+
|
|
287
|
+
const envText = renderServerEnvFile({
|
|
288
|
+
port: 3005,
|
|
289
|
+
host: '127.0.0.1',
|
|
290
|
+
platform: 'darwin',
|
|
291
|
+
arch: 'arm64',
|
|
292
|
+
serverBinDir,
|
|
293
|
+
dataDir: '/var/lib/happier',
|
|
294
|
+
filesDir: '/var/lib/happier/files',
|
|
295
|
+
dbDir: '/var/lib/happier/pglite',
|
|
296
|
+
});
|
|
297
|
+
assert.match(envText, /PRISMA_CLIENT_ENGINE_TYPE=library/);
|
|
298
|
+
assert.match(envText, new RegExp(`PRISMA_QUERY_ENGINE_LIBRARY=${enginePath.replaceAll('\\\\', '\\\\\\\\')}`));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('renderServerEnvFile uses file URL semantics on Windows', () => {
|
|
302
|
+
const envText = renderServerEnvFile({
|
|
303
|
+
port: 3005,
|
|
304
|
+
host: '127.0.0.1',
|
|
305
|
+
platform: 'win32',
|
|
306
|
+
dataDir: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host\\\\data',
|
|
307
|
+
filesDir: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host\\\\data\\\\files',
|
|
308
|
+
dbDir: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host\\\\data\\\\pglite',
|
|
309
|
+
});
|
|
310
|
+
assert.match(envText, /DATABASE_URL=file:\/\/\/C:\/Users\/me\/\.happier\/self-host\/data\/happier-server-light\.sqlite/);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('resolveSelfHostHealthTimeoutMs defaults to a safe health timeout', () => {
|
|
314
|
+
assert.equal(resolveSelfHostHealthTimeoutMs({}), 90_000);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('resolveSelfHostHealthTimeoutMs honors explicit timeout values >= 10s', () => {
|
|
318
|
+
assert.equal(resolveSelfHostHealthTimeoutMs({ HAPPIER_SELF_HOST_HEALTH_TIMEOUT_MS: '120000' }), 120_000);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('resolveSelfHostHealthTimeoutMs ignores invalid or too-small values', () => {
|
|
322
|
+
assert.equal(resolveSelfHostHealthTimeoutMs({ HAPPIER_SELF_HOST_HEALTH_TIMEOUT_MS: 'abc' }), 90_000);
|
|
323
|
+
assert.equal(resolveSelfHostHealthTimeoutMs({ HAPPIER_SELF_HOST_HEALTH_TIMEOUT_MS: '5000' }), 90_000);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('resolveSelfHostAutoUpdateDefault is opt-in (disabled by default)', () => {
|
|
327
|
+
assert.equal(resolveSelfHostAutoUpdateDefault({}), false);
|
|
328
|
+
assert.equal(resolveSelfHostAutoUpdateDefault({ HAPPIER_SELF_HOST_AUTO_UPDATE: '1' }), true);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('resolveSelfHostAutoUpdateIntervalMinutes provides a safe default and bounds invalid values', () => {
|
|
332
|
+
assert.equal(resolveSelfHostAutoUpdateIntervalMinutes({}), 1440);
|
|
333
|
+
assert.equal(resolveSelfHostAutoUpdateIntervalMinutes({ HAPPIER_SELF_HOST_AUTO_UPDATE_INTERVAL_MINUTES: '60' }), 60);
|
|
334
|
+
assert.equal(resolveSelfHostAutoUpdateIntervalMinutes({ HAPPIER_SELF_HOST_AUTO_UPDATE_INTERVAL_MINUTES: '0' }), 1440);
|
|
335
|
+
assert.equal(resolveSelfHostAutoUpdateIntervalMinutes({ HAPPIER_SELF_HOST_AUTO_UPDATE_INTERVAL_MINUTES: 'abc' }), 1440);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('renderUpdaterSystemdUnit runs self-host update without restart loops', () => {
|
|
339
|
+
const unit = renderUpdaterSystemdUnit({
|
|
340
|
+
updaterLabel: 'happier-server-updater',
|
|
341
|
+
hstackPath: '/home/me/.happier/bin/hstack',
|
|
342
|
+
channel: 'preview',
|
|
343
|
+
mode: 'user',
|
|
344
|
+
workingDirectory: '/home/me/.happier/self-host',
|
|
345
|
+
stdoutPath: '/home/me/.happier/self-host/logs/updater.out.log',
|
|
346
|
+
stderrPath: '/home/me/.happier/self-host/logs/updater.err.log',
|
|
347
|
+
wantedBy: 'default.target',
|
|
348
|
+
});
|
|
349
|
+
assert.match(unit, /ExecStart=\/home\/me\/\.happier\/bin\/hstack self-host update --channel=preview --mode=user --non-interactive/);
|
|
350
|
+
assert.match(unit, /Restart=no/);
|
|
351
|
+
assert.match(unit, /WantedBy=default\.target/);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('renderUpdaterLaunchdPlistXml runs self-host update without keepalive loops', () => {
|
|
355
|
+
const plist = renderUpdaterLaunchdPlistXml({
|
|
356
|
+
updaterLabel: 'happier-server-updater',
|
|
357
|
+
hstackPath: '/Users/me/.happier/bin/hstack',
|
|
358
|
+
channel: 'preview',
|
|
359
|
+
mode: 'user',
|
|
360
|
+
intervalMinutes: 60,
|
|
361
|
+
workingDirectory: '/Users/me/.happier/self-host',
|
|
362
|
+
stdoutPath: '/Users/me/.happier/self-host/logs/updater.out.log',
|
|
363
|
+
stderrPath: '/Users/me/.happier/self-host/logs/updater.err.log',
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
assert.match(plist, /<key>RunAtLoad<\/key>\s*<true\/>/);
|
|
367
|
+
assert.match(plist, /<key>StartInterval<\/key>\s*<integer>3600<\/integer>/);
|
|
368
|
+
assert.doesNotMatch(plist, /<key>StartCalendarInterval<\/key>/);
|
|
369
|
+
assert.doesNotMatch(plist, /<key>KeepAlive<\/key>/);
|
|
370
|
+
assert.match(plist, /<key>PATH<\/key>/);
|
|
371
|
+
assert.match(plist, /<string>\/Users\/me\/\.happier\/bin\/hstack<\/string>/);
|
|
372
|
+
assert.match(plist, /<string>self-host<\/string>/);
|
|
373
|
+
assert.match(plist, /<string>update<\/string>/);
|
|
374
|
+
assert.match(plist, /<string>--channel=preview<\/string>/);
|
|
375
|
+
assert.match(plist, /<string>--mode=user<\/string>/);
|
|
376
|
+
assert.match(plist, /<string>--non-interactive<\/string>/);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('renderUpdaterLaunchdPlistXml supports daily time-of-day schedules', () => {
|
|
380
|
+
const plist = renderUpdaterLaunchdPlistXml({
|
|
381
|
+
updaterLabel: 'happier-server-updater',
|
|
382
|
+
hstackPath: '/Users/me/.happier/bin/hstack',
|
|
383
|
+
channel: 'stable',
|
|
384
|
+
mode: 'user',
|
|
385
|
+
at: '03:15',
|
|
386
|
+
workingDirectory: '/Users/me/.happier/self-host',
|
|
387
|
+
stdoutPath: '/Users/me/.happier/self-host/logs/updater.out.log',
|
|
388
|
+
stderrPath: '/Users/me/.happier/self-host/logs/updater.err.log',
|
|
389
|
+
});
|
|
390
|
+
assert.match(plist, /<key>StartCalendarInterval<\/key>/);
|
|
391
|
+
assert.match(plist, /<key>Hour<\/key>\s*<integer>3<\/integer>/);
|
|
392
|
+
assert.match(plist, /<key>Minute<\/key>\s*<integer>15<\/integer>/);
|
|
393
|
+
assert.doesNotMatch(plist, /<key>StartInterval<\/key>/);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('renderUpdaterSystemdTimerUnit schedules periodic updater runs', () => {
|
|
397
|
+
const timer = renderUpdaterSystemdTimerUnit({
|
|
398
|
+
updaterLabel: 'happier-server-updater',
|
|
399
|
+
intervalMinutes: 60,
|
|
400
|
+
});
|
|
401
|
+
assert.match(timer, /OnUnitActiveSec=60m/);
|
|
402
|
+
assert.doesNotMatch(timer, /OnCalendar=/);
|
|
403
|
+
assert.match(timer, /Unit=happier-server-updater\.service/);
|
|
404
|
+
assert.match(timer, /WantedBy=timers\.target/);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('renderUpdaterSystemdTimerUnit supports daily time-of-day schedules', () => {
|
|
408
|
+
const timer = renderUpdaterSystemdTimerUnit({
|
|
409
|
+
updaterLabel: 'happier-server-updater',
|
|
410
|
+
at: '03:15',
|
|
411
|
+
});
|
|
412
|
+
assert.match(timer, /OnCalendar=\*-\*-\*\s+03:15:00/);
|
|
413
|
+
assert.doesNotMatch(timer, /OnUnitActiveSec=/);
|
|
414
|
+
assert.match(timer, /Unit=happier-server-updater\.service/);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('renderUpdaterScheduledTaskWrapperPs1 runs self-host update without node dependencies', () => {
|
|
418
|
+
const wrapper = renderUpdaterScheduledTaskWrapperPs1({
|
|
419
|
+
updaterLabel: 'happier-server-updater',
|
|
420
|
+
hstackPath: 'C:\\\\Users\\\\me\\\\.happier\\\\bin\\\\hstack.exe',
|
|
421
|
+
channel: 'preview',
|
|
422
|
+
mode: 'user',
|
|
423
|
+
workingDirectory: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host',
|
|
424
|
+
stdoutPath: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host\\\\logs\\\\updater.out.log',
|
|
425
|
+
stderrPath: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host\\\\logs\\\\updater.err.log',
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
assert.match(
|
|
429
|
+
wrapper,
|
|
430
|
+
/hstack\.exe"\s+"self-host"\s+"update"\s+"--channel=preview"\s+"--mode=user"\s+"--non-interactive"/i
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('buildUpdaterScheduledTaskCreateArgs uses DAILY schedule when at is provided', () => {
|
|
435
|
+
const args = buildUpdaterScheduledTaskCreateArgs({
|
|
436
|
+
backend: 'schtasks-user',
|
|
437
|
+
taskName: 'Happier\\\\happier-server-updater',
|
|
438
|
+
definitionPath: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host\\\\services\\\\happier-server-updater.ps1',
|
|
439
|
+
at: '03:15',
|
|
440
|
+
});
|
|
441
|
+
assert.ok(args.includes('DAILY'));
|
|
442
|
+
assert.ok(args.includes('03:15'));
|
|
443
|
+
assert.equal(args.includes('MINUTE'), false);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('mergeEnvTextWithDefaults preserves overrides while backfilling new default keys', () => {
|
|
447
|
+
const defaults = renderServerEnvFile({
|
|
448
|
+
port: 3005,
|
|
449
|
+
host: '127.0.0.1',
|
|
450
|
+
dataDir: '/var/lib/happier',
|
|
451
|
+
filesDir: '/var/lib/happier/files',
|
|
452
|
+
dbDir: '/var/lib/happier/pglite',
|
|
453
|
+
});
|
|
454
|
+
const existing = [
|
|
455
|
+
...defaults
|
|
456
|
+
.split('\n')
|
|
457
|
+
.filter((line) => !line.startsWith('HAPPIER_SQLITE_AUTO_MIGRATE=') && !line.startsWith('HAPPIER_SQLITE_MIGRATIONS_DIR=')),
|
|
458
|
+
'PORT=7777',
|
|
459
|
+
'FOO=bar',
|
|
460
|
+
'',
|
|
461
|
+
].join('\n');
|
|
462
|
+
|
|
463
|
+
const merged = mergeEnvTextWithDefaults(existing, defaults);
|
|
464
|
+
assert.match(merged, /PORT=7777/);
|
|
465
|
+
assert.match(merged, /HAPPIER_SQLITE_AUTO_MIGRATE=1/);
|
|
466
|
+
assert.match(merged, /HAPPIER_SQLITE_MIGRATIONS_DIR=\/var\/lib\/happier\/migrations\/sqlite/);
|
|
467
|
+
assert.match(merged, /FOO=bar/);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test('renderSelfHostStatusText reports versions, health, and auto-update config separately from job state', () => {
|
|
471
|
+
const text = renderSelfHostStatusText(
|
|
472
|
+
{
|
|
473
|
+
channel: 'preview',
|
|
474
|
+
mode: 'user',
|
|
475
|
+
serviceName: 'happier-server',
|
|
476
|
+
serverUrl: 'http://127.0.0.1:3005',
|
|
477
|
+
healthy: true,
|
|
478
|
+
service: { active: true, enabled: true },
|
|
479
|
+
versions: { server: '1.2.3-preview.1', uiWeb: '9.9.9-preview.2' },
|
|
480
|
+
autoUpdate: {
|
|
481
|
+
label: 'happier-server-updater',
|
|
482
|
+
job: { active: true, enabled: true },
|
|
483
|
+
configured: { enabled: true, intervalMinutes: 60 },
|
|
484
|
+
},
|
|
485
|
+
updatedAt: '2026-02-15T00:00:00.000Z',
|
|
486
|
+
},
|
|
487
|
+
{ colors: false },
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
assert.match(text, /channel:\s*preview/);
|
|
491
|
+
assert.match(text, /mode:\s*user/);
|
|
492
|
+
assert.match(text, /url:\s*http:\/\/127\.0\.0\.1:3005/);
|
|
493
|
+
assert.match(text, /health:\s*ok/);
|
|
494
|
+
assert.match(text, /server:\s*1\.2\.3-preview\.1/);
|
|
495
|
+
assert.match(text, /ui-web:\s*9\.9\.9-preview\.2/);
|
|
496
|
+
assert.match(text, /auto-update:\s*configured enabled \(every 60m\); job enabled, active/);
|
|
497
|
+
assert.match(text, /updated:\s*2026-02-15T00:00:00\.000Z/);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test('renderSelfHostStatusText shows disabled auto-update config even if job state is unknown', () => {
|
|
501
|
+
const text = renderSelfHostStatusText(
|
|
502
|
+
{
|
|
503
|
+
channel: 'stable',
|
|
504
|
+
mode: 'user',
|
|
505
|
+
serviceName: 'happier-server',
|
|
506
|
+
serverUrl: 'http://127.0.0.1:3005',
|
|
507
|
+
healthy: false,
|
|
508
|
+
service: { active: null, enabled: null },
|
|
509
|
+
versions: { server: null, uiWeb: null },
|
|
510
|
+
autoUpdate: {
|
|
511
|
+
label: 'happier-server-updater',
|
|
512
|
+
job: { active: null, enabled: null },
|
|
513
|
+
configured: { enabled: false, intervalMinutes: 1440 },
|
|
514
|
+
},
|
|
515
|
+
updatedAt: null,
|
|
516
|
+
},
|
|
517
|
+
{ colors: false },
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
assert.match(text, /auto-update:\s*configured disabled; job unknown/);
|
|
521
|
+
assert.match(text, /health:\s*failed/);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test('buildSelfHostDoctorChecks does not require external minisign and includes ui-web checks when installed', () => {
|
|
525
|
+
const checks = buildSelfHostDoctorChecks(
|
|
526
|
+
{
|
|
527
|
+
platform: 'linux',
|
|
528
|
+
mode: 'user',
|
|
529
|
+
serverBinaryPath: '/home/me/.happier/self-host/bin/happier-server',
|
|
530
|
+
configEnvPath: '/home/me/.happier/self-host/config/server.env',
|
|
531
|
+
uiWebCurrentDir: '/home/me/.happier/self-host/ui-web/current',
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
state: { uiWeb: { installed: true } },
|
|
535
|
+
commandExists: (name) => new Set(['tar', 'systemctl']).has(name),
|
|
536
|
+
pathExists: (p) => p.endsWith('happier-server') || p.endsWith('server.env') || p.endsWith('index.html'),
|
|
537
|
+
},
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
assert.ok(checks.find((c) => c.name === 'tar')?.ok);
|
|
541
|
+
assert.ok(checks.find((c) => c.name === 'systemctl')?.ok);
|
|
542
|
+
assert.equal(checks.some((c) => c.name === 'minisign'), false);
|
|
543
|
+
assert.ok(checks.find((c) => c.name === 'ui-web')?.ok);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test('buildSelfHostDoctorChecks flags missing ui-web bundle when state expects ui-web installed', () => {
|
|
547
|
+
const checks = buildSelfHostDoctorChecks(
|
|
548
|
+
{
|
|
549
|
+
platform: 'linux',
|
|
550
|
+
mode: 'user',
|
|
551
|
+
serverBinaryPath: '/home/me/.happier/self-host/bin/happier-server',
|
|
552
|
+
configEnvPath: '/home/me/.happier/self-host/config/server.env',
|
|
553
|
+
uiWebCurrentDir: '/home/me/.happier/self-host/ui-web/current',
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
state: { uiWeb: { installed: true } },
|
|
557
|
+
commandExists: () => true,
|
|
558
|
+
pathExists: (p) => !p.endsWith('index.html'),
|
|
559
|
+
},
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
assert.equal(checks.find((c) => c.name === 'ui-web')?.ok, false);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test('normalizeSelfHostAutoUpdateState upgrades legacy boolean config to structured config', () => {
|
|
566
|
+
assert.deepEqual(
|
|
567
|
+
normalizeSelfHostAutoUpdateState({ autoUpdate: true }, { fallbackIntervalMinutes: 1440 }),
|
|
568
|
+
{ enabled: true, intervalMinutes: 1440, at: '' },
|
|
569
|
+
);
|
|
570
|
+
assert.deepEqual(
|
|
571
|
+
normalizeSelfHostAutoUpdateState({ autoUpdate: false }, { fallbackIntervalMinutes: 1440 }),
|
|
572
|
+
{ enabled: false, intervalMinutes: 1440, at: '' },
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test('normalizeSelfHostAutoUpdateState preserves explicit interval and bounds invalid values', () => {
|
|
577
|
+
assert.deepEqual(
|
|
578
|
+
normalizeSelfHostAutoUpdateState({ autoUpdate: { enabled: true, intervalMinutes: 60 } }, { fallbackIntervalMinutes: 1440 }),
|
|
579
|
+
{ enabled: true, intervalMinutes: 60, at: '' },
|
|
580
|
+
);
|
|
581
|
+
assert.deepEqual(
|
|
582
|
+
normalizeSelfHostAutoUpdateState({ autoUpdate: { enabled: true, intervalMinutes: 0 } }, { fallbackIntervalMinutes: 1440 }),
|
|
583
|
+
{ enabled: true, intervalMinutes: 1440, at: '' },
|
|
584
|
+
);
|
|
585
|
+
assert.deepEqual(
|
|
586
|
+
normalizeSelfHostAutoUpdateState({}, { fallbackIntervalMinutes: 1440 }),
|
|
587
|
+
{ enabled: false, intervalMinutes: 1440, at: '' },
|
|
588
|
+
);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test('decideSelfHostAutoUpdateReconcile maps configured state to an install/uninstall action', () => {
|
|
592
|
+
assert.deepEqual(
|
|
593
|
+
decideSelfHostAutoUpdateReconcile({ autoUpdate: true }, { fallbackIntervalMinutes: 1440 }),
|
|
594
|
+
{ action: 'install', enabled: true, intervalMinutes: 1440, at: '' },
|
|
595
|
+
);
|
|
596
|
+
assert.deepEqual(
|
|
597
|
+
decideSelfHostAutoUpdateReconcile({ autoUpdate: false }, { fallbackIntervalMinutes: 1440 }),
|
|
598
|
+
{ action: 'uninstall', enabled: false, intervalMinutes: 1440, at: '' },
|
|
599
|
+
);
|
|
600
|
+
assert.deepEqual(
|
|
601
|
+
decideSelfHostAutoUpdateReconcile({}, { fallbackIntervalMinutes: 1440 }),
|
|
602
|
+
{ action: 'uninstall', enabled: false, intervalMinutes: 1440, at: '' },
|
|
603
|
+
);
|
|
604
|
+
});
|