@codori/server 0.0.2 → 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.
- package/README.md +34 -0
- package/client-dist/200.html +1 -1
- package/client-dist/404.html +1 -1
- package/client-dist/_nuxt/5cGRqVd7.js +1 -0
- package/client-dist/_nuxt/B1wExIrb.js +3 -0
- package/client-dist/_nuxt/{bTgPCKPF.js → B3F7AEX8.js} +1 -1
- package/client-dist/_nuxt/BLQDSwSv.js +1 -0
- package/client-dist/_nuxt/CNJnWNJ6.js +1 -0
- package/client-dist/_nuxt/CTkc1dr0.js +30 -0
- package/client-dist/_nuxt/Ce2C-7Tv.js +1 -0
- package/client-dist/_nuxt/CnVIfmli.js +1 -0
- package/client-dist/_nuxt/DQ92n70Y.js +202 -0
- package/client-dist/_nuxt/{BqBt7DE7.js → DfKxoeGc.js} +1 -1
- package/client-dist/_nuxt/VKebgJ9X.js +1 -0
- package/client-dist/_nuxt/_6KBkEBa.js +1 -0
- package/client-dist/_nuxt/_threadId_.BfTZeVnD.css +1 -0
- package/client-dist/_nuxt/builds/latest.json +1 -1
- package/client-dist/_nuxt/builds/meta/9f29d3f8-cf52-453d-9a00-836b5db2a30e.json +1 -0
- package/client-dist/_nuxt/entry.BFUss7SH.css +1 -0
- package/client-dist/index.html +1 -1
- package/dist/attachment-store.d.ts +32 -0
- package/dist/attachment-store.js +84 -0
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +171 -19
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -2
- package/dist/http-server.d.ts +3 -0
- package/dist/http-server.js +181 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/service-adapters.d.ts +39 -0
- package/dist/service-adapters.js +185 -0
- package/dist/service-update.d.ts +26 -0
- package/dist/service-update.js +196 -0
- package/dist/service.d.ts +86 -0
- package/dist/service.js +616 -0
- package/package.json +6 -1
- package/client-dist/_nuxt/7-GD2bnK.js +0 -1
- package/client-dist/_nuxt/B3mzt2bn.js +0 -1
- package/client-dist/_nuxt/BC-nNqet.js +0 -1
- package/client-dist/_nuxt/BZ0PKZpG.js +0 -1
- package/client-dist/_nuxt/BtYrONTB.js +0 -30
- package/client-dist/_nuxt/CG5UFFba.js +0 -203
- package/client-dist/_nuxt/CcHTZT9i.js +0 -1
- package/client-dist/_nuxt/D39j61CJ.js +0 -1
- package/client-dist/_nuxt/Dgfnd7_d.js +0 -1
- package/client-dist/_nuxt/_threadId_.DWwkJvLa.css +0 -1
- package/client-dist/_nuxt/builds/meta/3230b6da-fb0f-48a4-a654-1a7f8145eecd.json +0 -1
- package/client-dist/_nuxt/entry.V8kD4EEO.css +0 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ServicePlatform, ServiceScope } from './service.js';
|
|
2
|
+
export type ServiceCommand = {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
};
|
|
6
|
+
export type ServiceUnitDefinition = {
|
|
7
|
+
serviceName: string;
|
|
8
|
+
serviceFilePath: string;
|
|
9
|
+
serviceFileContents: string;
|
|
10
|
+
};
|
|
11
|
+
export type ServiceUnitInput = {
|
|
12
|
+
installId: string;
|
|
13
|
+
scope: ServiceScope;
|
|
14
|
+
launcherPath: string;
|
|
15
|
+
root: string;
|
|
16
|
+
metadataDirectory: string;
|
|
17
|
+
homeDir?: string;
|
|
18
|
+
userId?: number;
|
|
19
|
+
};
|
|
20
|
+
export declare const resolveServicePlatform: (platform?: NodeJS.Platform) => ServicePlatform;
|
|
21
|
+
export declare const getDarwinServiceName: (installId: string) => string;
|
|
22
|
+
export declare const getLinuxServiceName: (installId: string) => string;
|
|
23
|
+
export declare const renderLaunchdPlist: ({ serviceName, launcherPath, root, metadataDirectory }: Omit<ServiceUnitDefinition, "serviceFilePath" | "serviceFileContents"> & {
|
|
24
|
+
launcherPath: string;
|
|
25
|
+
root: string;
|
|
26
|
+
metadataDirectory: string;
|
|
27
|
+
}) => string;
|
|
28
|
+
export declare const renderSystemdUnit: ({ serviceName, launcherPath, root }: Omit<ServiceUnitDefinition, "serviceFilePath" | "serviceFileContents"> & {
|
|
29
|
+
launcherPath: string;
|
|
30
|
+
root: string;
|
|
31
|
+
}) => string;
|
|
32
|
+
export declare const createDarwinServiceDefinition: ({ installId, scope, launcherPath, root, metadataDirectory, homeDir }: ServiceUnitInput) => ServiceUnitDefinition;
|
|
33
|
+
export declare const createLinuxServiceDefinition: ({ installId, scope, launcherPath, root, homeDir }: ServiceUnitInput) => ServiceUnitDefinition;
|
|
34
|
+
export declare const getDarwinInstallCommands: (definition: ServiceUnitDefinition, scope: ServiceScope, userId?: number) => ServiceCommand[];
|
|
35
|
+
export declare const getDarwinRestartCommands: (definition: ServiceUnitDefinition, scope: ServiceScope, userId?: number) => ServiceCommand[];
|
|
36
|
+
export declare const getDarwinUninstallCommands: (definition: ServiceUnitDefinition, scope: ServiceScope, userId?: number) => ServiceCommand[];
|
|
37
|
+
export declare const getLinuxInstallCommands: (definition: ServiceUnitDefinition, scope: ServiceScope) => ServiceCommand[];
|
|
38
|
+
export declare const getLinuxRestartCommands: (definition: ServiceUnitDefinition, scope: ServiceScope) => ServiceCommand[];
|
|
39
|
+
export declare const getLinuxUninstallCommands: (definition: ServiceUnitDefinition, scope: ServiceScope) => ServiceCommand[];
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const renderLaunchdArray = (values) => values.map(value => ` <string>${escapeXml(value)}</string>`).join('\n');
|
|
4
|
+
const escapeXml = (value) => value
|
|
5
|
+
.replaceAll('&', '&')
|
|
6
|
+
.replaceAll('<', '<')
|
|
7
|
+
.replaceAll('>', '>')
|
|
8
|
+
.replaceAll('"', '"')
|
|
9
|
+
.replaceAll("'", ''');
|
|
10
|
+
const getCurrentUserId = () => (typeof process.getuid === 'function' ? process.getuid() : 0);
|
|
11
|
+
const getLaunchctlDomain = (scope, userId = getCurrentUserId()) => scope === 'system' ? 'system' : `gui/${userId}`;
|
|
12
|
+
const getSystemdPrefix = (scope) => scope === 'system' ? [] : ['--user'];
|
|
13
|
+
const quoteSystemdPathValue = (value) => `"${value
|
|
14
|
+
.replaceAll('\\', '\\\\')
|
|
15
|
+
.replaceAll('"', '\\"')
|
|
16
|
+
.replaceAll('%', '%%')}"`;
|
|
17
|
+
const quoteSystemdExecValue = (value) => quoteSystemdPathValue(value).replaceAll('$', '$$$$');
|
|
18
|
+
export const resolveServicePlatform = (platform = process.platform) => {
|
|
19
|
+
if (platform === 'darwin' || platform === 'linux') {
|
|
20
|
+
return platform;
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Unsupported service platform "${platform}".`);
|
|
23
|
+
};
|
|
24
|
+
export const getDarwinServiceName = (installId) => `io.codori.server.${installId}`;
|
|
25
|
+
export const getLinuxServiceName = (installId) => `codori-${installId}.service`;
|
|
26
|
+
export const renderLaunchdPlist = ({ serviceName, launcherPath, root, metadataDirectory }) => [
|
|
27
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
28
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
29
|
+
'<plist version="1.0">',
|
|
30
|
+
'<dict>',
|
|
31
|
+
' <key>Label</key>',
|
|
32
|
+
` <string>${escapeXml(serviceName)}</string>`,
|
|
33
|
+
' <key>ProgramArguments</key>',
|
|
34
|
+
' <array>',
|
|
35
|
+
renderLaunchdArray([launcherPath]),
|
|
36
|
+
' </array>',
|
|
37
|
+
' <key>RunAtLoad</key>',
|
|
38
|
+
' <true/>',
|
|
39
|
+
' <key>KeepAlive</key>',
|
|
40
|
+
' <true/>',
|
|
41
|
+
' <key>WorkingDirectory</key>',
|
|
42
|
+
` <string>${escapeXml(root)}</string>`,
|
|
43
|
+
' <key>StandardOutPath</key>',
|
|
44
|
+
` <string>${escapeXml(join(metadataDirectory, 'service.log'))}</string>`,
|
|
45
|
+
' <key>StandardErrorPath</key>',
|
|
46
|
+
` <string>${escapeXml(join(metadataDirectory, 'service.error.log'))}</string>`,
|
|
47
|
+
'</dict>',
|
|
48
|
+
'</plist>'
|
|
49
|
+
].join('\n');
|
|
50
|
+
export const renderSystemdUnit = ({ serviceName, launcherPath, root }) => [
|
|
51
|
+
'[Unit]',
|
|
52
|
+
`Description=Codori service (${serviceName})`,
|
|
53
|
+
'After=network.target',
|
|
54
|
+
'',
|
|
55
|
+
'[Service]',
|
|
56
|
+
'Type=simple',
|
|
57
|
+
`WorkingDirectory=${quoteSystemdPathValue(root)}`,
|
|
58
|
+
`ExecStart=${quoteSystemdExecValue(launcherPath)}`,
|
|
59
|
+
'Restart=always',
|
|
60
|
+
'RestartSec=5',
|
|
61
|
+
'',
|
|
62
|
+
'[Install]',
|
|
63
|
+
'WantedBy=default.target'
|
|
64
|
+
].join('\n');
|
|
65
|
+
export const createDarwinServiceDefinition = ({ installId, scope, launcherPath, root, metadataDirectory, homeDir = os.homedir() }) => {
|
|
66
|
+
const serviceName = getDarwinServiceName(installId);
|
|
67
|
+
const serviceFilePath = scope === 'system'
|
|
68
|
+
? join('/Library/LaunchDaemons', `${serviceName}.plist`)
|
|
69
|
+
: join(homeDir, 'Library', 'LaunchAgents', `${serviceName}.plist`);
|
|
70
|
+
return {
|
|
71
|
+
serviceName,
|
|
72
|
+
serviceFilePath,
|
|
73
|
+
serviceFileContents: renderLaunchdPlist({
|
|
74
|
+
serviceName,
|
|
75
|
+
launcherPath,
|
|
76
|
+
root,
|
|
77
|
+
metadataDirectory
|
|
78
|
+
})
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
export const createLinuxServiceDefinition = ({ installId, scope, launcherPath, root, homeDir = os.homedir() }) => {
|
|
82
|
+
const serviceName = getLinuxServiceName(installId);
|
|
83
|
+
const serviceFilePath = scope === 'system'
|
|
84
|
+
? join('/etc/systemd/system', serviceName)
|
|
85
|
+
: join(homeDir, '.config', 'systemd', 'user', serviceName);
|
|
86
|
+
return {
|
|
87
|
+
serviceName,
|
|
88
|
+
serviceFilePath,
|
|
89
|
+
serviceFileContents: renderSystemdUnit({
|
|
90
|
+
serviceName,
|
|
91
|
+
launcherPath,
|
|
92
|
+
root
|
|
93
|
+
})
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
export const getDarwinInstallCommands = (definition, scope, userId = getCurrentUserId()) => {
|
|
97
|
+
const domain = getLaunchctlDomain(scope, userId);
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
command: 'launchctl',
|
|
101
|
+
args: ['bootout', domain, definition.serviceFilePath]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
command: 'launchctl',
|
|
105
|
+
args: ['bootstrap', domain, definition.serviceFilePath]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
command: 'launchctl',
|
|
109
|
+
args: ['enable', `${domain}/${definition.serviceName}`]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
command: 'launchctl',
|
|
113
|
+
args: ['kickstart', '-k', `${domain}/${definition.serviceName}`]
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
};
|
|
117
|
+
export const getDarwinRestartCommands = (definition, scope, userId = getCurrentUserId()) => {
|
|
118
|
+
const domain = getLaunchctlDomain(scope, userId);
|
|
119
|
+
return [
|
|
120
|
+
{
|
|
121
|
+
command: 'launchctl',
|
|
122
|
+
args: ['bootout', domain, definition.serviceFilePath]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
command: 'launchctl',
|
|
126
|
+
args: ['bootstrap', domain, definition.serviceFilePath]
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
command: 'launchctl',
|
|
130
|
+
args: ['kickstart', '-k', `${domain}/${definition.serviceName}`]
|
|
131
|
+
}
|
|
132
|
+
];
|
|
133
|
+
};
|
|
134
|
+
export const getDarwinUninstallCommands = (definition, scope, userId = getCurrentUserId()) => {
|
|
135
|
+
const domain = getLaunchctlDomain(scope, userId);
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
command: 'launchctl',
|
|
139
|
+
args: ['bootout', domain, definition.serviceFilePath]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
command: 'launchctl',
|
|
143
|
+
args: ['disable', `${domain}/${definition.serviceName}`]
|
|
144
|
+
}
|
|
145
|
+
];
|
|
146
|
+
};
|
|
147
|
+
export const getLinuxInstallCommands = (definition, scope) => {
|
|
148
|
+
const prefix = getSystemdPrefix(scope);
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
command: 'systemctl',
|
|
152
|
+
args: [...prefix, 'daemon-reload']
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
command: 'systemctl',
|
|
156
|
+
args: [...prefix, 'enable', '--now', definition.serviceName]
|
|
157
|
+
}
|
|
158
|
+
];
|
|
159
|
+
};
|
|
160
|
+
export const getLinuxRestartCommands = (definition, scope) => {
|
|
161
|
+
const prefix = getSystemdPrefix(scope);
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
command: 'systemctl',
|
|
165
|
+
args: [...prefix, 'daemon-reload']
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
command: 'systemctl',
|
|
169
|
+
args: [...prefix, 'restart', definition.serviceName]
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
};
|
|
173
|
+
export const getLinuxUninstallCommands = (definition, scope) => {
|
|
174
|
+
const prefix = getSystemdPrefix(scope);
|
|
175
|
+
return [
|
|
176
|
+
{
|
|
177
|
+
command: 'systemctl',
|
|
178
|
+
args: [...prefix, 'disable', '--now', definition.serviceName]
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
command: 'systemctl',
|
|
182
|
+
args: [...prefix, 'daemon-reload']
|
|
183
|
+
}
|
|
184
|
+
];
|
|
185
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ServiceUpdateStatus = {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
updateAvailable: boolean;
|
|
4
|
+
updating: boolean;
|
|
5
|
+
installedVersion: string | null;
|
|
6
|
+
latestVersion: string | null;
|
|
7
|
+
};
|
|
8
|
+
export type ServiceUpdateController = {
|
|
9
|
+
getStatus: () => Promise<ServiceUpdateStatus>;
|
|
10
|
+
requestUpdate: () => Promise<ServiceUpdateStatus>;
|
|
11
|
+
};
|
|
12
|
+
export type ServiceUpdateControllerOptions = {
|
|
13
|
+
root: string;
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
homeDir?: string;
|
|
16
|
+
now?: () => number;
|
|
17
|
+
npxPath?: string;
|
|
18
|
+
fetchImpl?: typeof fetch;
|
|
19
|
+
cacheTtlMs?: number;
|
|
20
|
+
registryTimeoutMs?: number;
|
|
21
|
+
spawnUpdateProcess?: (command: string, args: string[], options: {
|
|
22
|
+
env: NodeJS.ProcessEnv;
|
|
23
|
+
}) => Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
export declare const comparePackageVersions: (left: string, right: string) => number;
|
|
26
|
+
export declare const createServiceUpdateController: (options: ServiceUpdateControllerOptions) => ServiceUpdateController;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { CodoriError } from './errors.js';
|
|
7
|
+
import { CODORI_SERVICE_INSTALL_ID_ENV, CODORI_SERVICE_MANAGED_ENV, CODORI_SERVICE_SCOPE_ENV, getServiceMetadataDirectory } from './service.js';
|
|
8
|
+
const PACKAGE_MANIFEST_PATH = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
9
|
+
const UPDATE_CHECK_TTL_MS = 5 * 60 * 1_000;
|
|
10
|
+
const REGISTRY_TIMEOUT_MS = 3_000;
|
|
11
|
+
const shellEscape = (value) => `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
12
|
+
const readPackageManifest = () => {
|
|
13
|
+
const parsed = JSON.parse(readFileSync(PACKAGE_MANIFEST_PATH, 'utf8'));
|
|
14
|
+
if (typeof parsed !== 'object'
|
|
15
|
+
|| parsed === null
|
|
16
|
+
|| typeof parsed.name !== 'string'
|
|
17
|
+
|| typeof parsed.version !== 'string') {
|
|
18
|
+
throw new Error(`Invalid package manifest at ${PACKAGE_MANIFEST_PATH}.`);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
name: parsed.name,
|
|
22
|
+
version: parsed.version
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
const CURRENT_PACKAGE = readPackageManifest();
|
|
26
|
+
const DISABLED_STATUS = {
|
|
27
|
+
enabled: false,
|
|
28
|
+
updateAvailable: false,
|
|
29
|
+
updating: false,
|
|
30
|
+
installedVersion: null,
|
|
31
|
+
latestVersion: null
|
|
32
|
+
};
|
|
33
|
+
const coerceVersionPart = (value) => {
|
|
34
|
+
if (/^\d+$/u.test(value)) {
|
|
35
|
+
return Number.parseInt(value, 10);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
export const comparePackageVersions = (left, right) => {
|
|
40
|
+
const maxLength = Math.max(left.split('.').length, right.split('.').length);
|
|
41
|
+
const leftParts = left.split('.').map(coerceVersionPart);
|
|
42
|
+
const rightParts = right.split('.').map(coerceVersionPart);
|
|
43
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
44
|
+
const leftPart = leftParts[index] ?? 0;
|
|
45
|
+
const rightPart = rightParts[index] ?? 0;
|
|
46
|
+
if (leftPart === rightPart) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (typeof leftPart === 'number' && typeof rightPart === 'number') {
|
|
50
|
+
return leftPart > rightPart ? 1 : -1;
|
|
51
|
+
}
|
|
52
|
+
return String(leftPart).localeCompare(String(rightPart), undefined, { numeric: true });
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
};
|
|
56
|
+
const resolveServiceRuntimeContext = (env) => {
|
|
57
|
+
if (env[CODORI_SERVICE_MANAGED_ENV] !== '1') {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const installId = env[CODORI_SERVICE_INSTALL_ID_ENV]?.trim();
|
|
61
|
+
const scope = env[CODORI_SERVICE_SCOPE_ENV]?.trim();
|
|
62
|
+
if (!installId || (scope !== 'user' && scope !== 'system')) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
installId,
|
|
67
|
+
scope
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
const fetchLatestPackageVersion = async (fetchImpl, registryTimeoutMs) => {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const timeout = setTimeout(() => {
|
|
73
|
+
controller.abort();
|
|
74
|
+
}, registryTimeoutMs);
|
|
75
|
+
let response;
|
|
76
|
+
try {
|
|
77
|
+
response = await fetchImpl(`https://registry.npmjs.org/${encodeURIComponent(CURRENT_PACKAGE.name)}/latest`, {
|
|
78
|
+
headers: {
|
|
79
|
+
accept: 'application/json'
|
|
80
|
+
},
|
|
81
|
+
signal: controller.signal
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
}
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(`npm registry request failed with status ${response.status}.`);
|
|
89
|
+
}
|
|
90
|
+
const payload = await response.json();
|
|
91
|
+
if (typeof payload !== 'object' || payload === null || typeof payload.version !== 'string') {
|
|
92
|
+
throw new Error('npm registry response did not include a valid version.');
|
|
93
|
+
}
|
|
94
|
+
return payload.version;
|
|
95
|
+
};
|
|
96
|
+
const createUpdateScript = (runtime, options) => {
|
|
97
|
+
const metadataDirectory = getServiceMetadataDirectory(runtime.installId, options.homeDir);
|
|
98
|
+
const updateLogPath = join(metadataDirectory, 'update.log');
|
|
99
|
+
return [
|
|
100
|
+
'sleep 1',
|
|
101
|
+
`${shellEscape(options.npxPath)} --yes ${shellEscape(`${CURRENT_PACKAGE.name}@latest`)} restart-service --root ${shellEscape(options.root)} --scope ${shellEscape(runtime.scope)} --yes >> ${shellEscape(updateLogPath)} 2>&1`
|
|
102
|
+
].join('; ');
|
|
103
|
+
};
|
|
104
|
+
const defaultSpawnUpdateProcess = async (command, args, options) => {
|
|
105
|
+
await new Promise((resolvePromise, reject) => {
|
|
106
|
+
const child = spawn(command, args, {
|
|
107
|
+
detached: true,
|
|
108
|
+
env: options.env,
|
|
109
|
+
stdio: 'ignore',
|
|
110
|
+
windowsHide: true
|
|
111
|
+
});
|
|
112
|
+
child.once('error', reject);
|
|
113
|
+
child.once('spawn', () => {
|
|
114
|
+
child.unref();
|
|
115
|
+
resolvePromise();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
export const createServiceUpdateController = (options) => {
|
|
120
|
+
const env = options.env ?? process.env;
|
|
121
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
122
|
+
const npxPath = options.npxPath ?? join(dirname(process.execPath), 'npx');
|
|
123
|
+
const now = options.now ?? (() => Date.now());
|
|
124
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
125
|
+
const cacheTtlMs = options.cacheTtlMs ?? UPDATE_CHECK_TTL_MS;
|
|
126
|
+
const registryTimeoutMs = options.registryTimeoutMs ?? REGISTRY_TIMEOUT_MS;
|
|
127
|
+
const spawnUpdateProcess = options.spawnUpdateProcess ?? defaultSpawnUpdateProcess;
|
|
128
|
+
const runtime = resolveServiceRuntimeContext(env);
|
|
129
|
+
let updating = false;
|
|
130
|
+
let cachedStatus = null;
|
|
131
|
+
let cachedAt = 0;
|
|
132
|
+
let pendingStatus = null;
|
|
133
|
+
const resolveStatus = async () => {
|
|
134
|
+
if (!runtime) {
|
|
135
|
+
return DISABLED_STATUS;
|
|
136
|
+
}
|
|
137
|
+
if (pendingStatus) {
|
|
138
|
+
return pendingStatus;
|
|
139
|
+
}
|
|
140
|
+
if (cachedStatus && now() - cachedAt < cacheTtlMs) {
|
|
141
|
+
return {
|
|
142
|
+
...cachedStatus,
|
|
143
|
+
updating
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
pendingStatus = (async () => {
|
|
147
|
+
let latestVersion = cachedStatus?.latestVersion ?? null;
|
|
148
|
+
try {
|
|
149
|
+
latestVersion = await fetchLatestPackageVersion(fetchImpl, registryTimeoutMs);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
latestVersion = cachedStatus?.latestVersion ?? null;
|
|
153
|
+
}
|
|
154
|
+
const nextStatus = {
|
|
155
|
+
enabled: true,
|
|
156
|
+
updateAvailable: latestVersion !== null && comparePackageVersions(latestVersion, CURRENT_PACKAGE.version) > 0,
|
|
157
|
+
updating,
|
|
158
|
+
installedVersion: CURRENT_PACKAGE.version,
|
|
159
|
+
latestVersion
|
|
160
|
+
};
|
|
161
|
+
cachedStatus = nextStatus;
|
|
162
|
+
cachedAt = now();
|
|
163
|
+
pendingStatus = null;
|
|
164
|
+
return nextStatus;
|
|
165
|
+
})();
|
|
166
|
+
return pendingStatus;
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
getStatus: async () => await resolveStatus(),
|
|
170
|
+
requestUpdate: async () => {
|
|
171
|
+
if (!runtime) {
|
|
172
|
+
throw new CodoriError('SERVICE_UPDATE_UNAVAILABLE', 'Self-update is only available while Codori is running as a registered service.');
|
|
173
|
+
}
|
|
174
|
+
if (updating) {
|
|
175
|
+
throw new CodoriError('SERVICE_UPDATE_IN_PROGRESS', 'Codori is already applying a service update.');
|
|
176
|
+
}
|
|
177
|
+
const status = await resolveStatus();
|
|
178
|
+
if (!status.updateAvailable) {
|
|
179
|
+
throw new CodoriError('SERVICE_UPDATE_UNAVAILABLE', 'No newer @codori/server package is currently available.');
|
|
180
|
+
}
|
|
181
|
+
const script = createUpdateScript(runtime, {
|
|
182
|
+
root: options.root,
|
|
183
|
+
npxPath,
|
|
184
|
+
homeDir
|
|
185
|
+
});
|
|
186
|
+
await spawnUpdateProcess('/bin/sh', ['-lc', script], { env });
|
|
187
|
+
updating = true;
|
|
188
|
+
cachedStatus = {
|
|
189
|
+
...status,
|
|
190
|
+
updating: true
|
|
191
|
+
};
|
|
192
|
+
cachedAt = now();
|
|
193
|
+
return cachedStatus;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type ServiceScope = 'user' | 'system';
|
|
2
|
+
export type ServicePlatform = 'darwin' | 'linux';
|
|
3
|
+
export type ServiceInstallMetadata = {
|
|
4
|
+
installId: string;
|
|
5
|
+
root: string;
|
|
6
|
+
host: string;
|
|
7
|
+
port: number;
|
|
8
|
+
scope: ServiceScope;
|
|
9
|
+
platform: ServicePlatform;
|
|
10
|
+
serviceName: string;
|
|
11
|
+
serviceFilePath: string;
|
|
12
|
+
launcherPath: string;
|
|
13
|
+
installedAt: string;
|
|
14
|
+
};
|
|
15
|
+
export type RootPromptDefault = {
|
|
16
|
+
value: string | null;
|
|
17
|
+
reason: 'git' | 'workspace-marker' | 'nested-git-projects' | 'none';
|
|
18
|
+
shouldConfirm: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type HostPromptDefault = {
|
|
21
|
+
value: string;
|
|
22
|
+
source: 'explicit' | 'tailscale' | 'wildcard';
|
|
23
|
+
warning: string | null;
|
|
24
|
+
};
|
|
25
|
+
export type CommandResult = {
|
|
26
|
+
exitCode: number | null;
|
|
27
|
+
stdout: string;
|
|
28
|
+
stderr: string;
|
|
29
|
+
};
|
|
30
|
+
export type CommandRunner = (command: string, args: string[]) => Promise<CommandResult>;
|
|
31
|
+
export type LauncherScriptInput = {
|
|
32
|
+
installId: string;
|
|
33
|
+
root: string;
|
|
34
|
+
host: string;
|
|
35
|
+
port: number;
|
|
36
|
+
scope: ServiceScope;
|
|
37
|
+
nodePath: string;
|
|
38
|
+
npxPath: string;
|
|
39
|
+
};
|
|
40
|
+
export type ServicePrompt = {
|
|
41
|
+
input: (message: string, defaultValue?: string) => Promise<string>;
|
|
42
|
+
confirm: (message: string, defaultValue: boolean) => Promise<boolean>;
|
|
43
|
+
close: () => Promise<void> | void;
|
|
44
|
+
};
|
|
45
|
+
export type ServiceCommandName = 'install-service' | 'setup-service' | 'restart-service' | 'uninstall-service';
|
|
46
|
+
export type ServiceCommandOptions = {
|
|
47
|
+
root?: string;
|
|
48
|
+
host?: string;
|
|
49
|
+
port?: string | number;
|
|
50
|
+
scope?: string;
|
|
51
|
+
yes?: boolean;
|
|
52
|
+
};
|
|
53
|
+
export type ServiceCommandDependencies = {
|
|
54
|
+
cwd?: string;
|
|
55
|
+
homeDir?: string;
|
|
56
|
+
platform?: NodeJS.Platform;
|
|
57
|
+
nodePath?: string;
|
|
58
|
+
npxPath?: string;
|
|
59
|
+
stdin?: NodeJS.ReadableStream;
|
|
60
|
+
stdout?: NodeJS.WritableStream;
|
|
61
|
+
runCommand?: CommandRunner;
|
|
62
|
+
prompt?: ServicePrompt;
|
|
63
|
+
now?: () => Date;
|
|
64
|
+
};
|
|
65
|
+
export type ServiceOperationResult = {
|
|
66
|
+
action: 'install' | 'restart' | 'uninstall';
|
|
67
|
+
metadata: ServiceInstallMetadata;
|
|
68
|
+
};
|
|
69
|
+
export declare const CODORI_SERVICE_MANAGED_ENV = "CODORI_SERVICE_MANAGED";
|
|
70
|
+
export declare const CODORI_SERVICE_INSTALL_ID_ENV = "CODORI_SERVICE_INSTALL_ID";
|
|
71
|
+
export declare const CODORI_SERVICE_SCOPE_ENV = "CODORI_SERVICE_SCOPE";
|
|
72
|
+
export declare const toServiceInstallId: (root: string) => string;
|
|
73
|
+
export declare const getServiceMetadataDirectory: (installId: string, homeDir?: string) => string;
|
|
74
|
+
export declare const getServiceMetadataPath: (installId: string, homeDir?: string) => string;
|
|
75
|
+
export declare const getServiceLauncherPath: (installId: string, homeDir?: string) => string;
|
|
76
|
+
export declare const detectRootPromptDefault: (cwd: string) => RootPromptDefault;
|
|
77
|
+
export declare const resolveServiceScope: (value: string | undefined) => ServiceScope;
|
|
78
|
+
export declare const parseServicePort: (value: string | number | undefined) => number | undefined;
|
|
79
|
+
export declare const resolveDefaultServicePort: (value: number | undefined) => number;
|
|
80
|
+
export declare const getWildcardHostWarning: () => string;
|
|
81
|
+
export declare const detectTailscaleIpv4: (runCommand?: CommandRunner) => Promise<string | null>;
|
|
82
|
+
export declare const resolveHostPromptDefault: (explicitHost: string | undefined, runCommand?: CommandRunner) => Promise<HostPromptDefault>;
|
|
83
|
+
export declare const buildLauncherScript: ({ installId, root, host, port, scope, nodePath, npxPath }: LauncherScriptInput) => string;
|
|
84
|
+
export declare const installService: (options?: ServiceCommandOptions, dependencies?: ServiceCommandDependencies) => Promise<ServiceOperationResult>;
|
|
85
|
+
export declare const restartService: (options?: ServiceCommandOptions, dependencies?: ServiceCommandDependencies) => Promise<ServiceOperationResult>;
|
|
86
|
+
export declare const uninstallService: (options?: ServiceCommandOptions, dependencies?: ServiceCommandDependencies) => Promise<ServiceOperationResult>;
|