neoagent 2.1.14 → 2.1.15
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 +1 -1
- package/lib/manager.js +109 -13
- package/package.json +1 -1
- package/runtime/release_channel.js +123 -0
- package/server/index.js +9 -4
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +32583 -32469
- package/server/routes/settings.js +32 -1
- package/server/services/bootstrap_helpers.js +54 -0
- package/server/services/manager.js +290 -154
- package/server/utils/version.js +17 -1
package/README.md
CHANGED
|
@@ -21,13 +21,13 @@ neoagent install
|
|
|
21
21
|
Manage the service:
|
|
22
22
|
```bash
|
|
23
23
|
neoagent status
|
|
24
|
+
neoagent channel beta
|
|
24
25
|
neoagent update
|
|
25
26
|
neoagent fix
|
|
26
27
|
neoagent logs
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
Use `neoagent fix` if a self-edit or broken local install leaves NeoAgent in a bad state. On git installs it backs up runtime data, saves local tracked changes, resets tracked source files, reinstalls dependencies, and restarts the service.
|
|
30
|
-
|
|
31
31
|
---
|
|
32
32
|
|
|
33
33
|
[⚙️ Configuration](docs/configuration.md) · [🧰 Skills](docs/skills.md) · [🐛 Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
|
package/lib/manager.js
CHANGED
|
@@ -21,6 +21,14 @@ const {
|
|
|
21
21
|
ensureRuntimeDirs,
|
|
22
22
|
migrateLegacyRuntime
|
|
23
23
|
} = require('../runtime/paths');
|
|
24
|
+
const {
|
|
25
|
+
parseReleaseChannel,
|
|
26
|
+
getReleaseChannelBranch,
|
|
27
|
+
getReleaseChannelDistTag,
|
|
28
|
+
getReleaseChannelLabel,
|
|
29
|
+
readConfiguredReleaseChannel,
|
|
30
|
+
writeReleaseChannelToEnvFile,
|
|
31
|
+
} = require('../runtime/release_channel');
|
|
24
32
|
|
|
25
33
|
const APP_NAME = 'NeoAgent';
|
|
26
34
|
const SERVICE_LABEL = 'com.neoagent';
|
|
@@ -184,6 +192,59 @@ function commandExists(cmd) {
|
|
|
184
192
|
return sharedCommandExists((command, args) => runQuiet(command, args), cmd);
|
|
185
193
|
}
|
|
186
194
|
|
|
195
|
+
function currentReleaseChannel() {
|
|
196
|
+
return readConfiguredReleaseChannel({ envFile: ENV_FILE });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function releaseChannelSummary(channel) {
|
|
200
|
+
const normalized = parseReleaseChannel(channel) || currentReleaseChannel();
|
|
201
|
+
return `${getReleaseChannelLabel(normalized)} (branch ${getReleaseChannelBranch(normalized)}, npm ${getReleaseChannelDistTag(normalized)})`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function gitWorkingTreeDirty() {
|
|
205
|
+
const res = runQuiet('git', ['status', '--porcelain']);
|
|
206
|
+
return res.status === 0 && Boolean(res.stdout.trim());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function gitLocalBranchExists(branch) {
|
|
210
|
+
return runQuiet('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]).status === 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function gitRemoteBranchExists(branch) {
|
|
214
|
+
return runQuiet('git', ['ls-remote', '--exit-code', '--heads', 'origin', branch]).status === 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function ensureGitBranchForReleaseChannel(targetBranch) {
|
|
218
|
+
const branchRes = runQuiet('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
219
|
+
const currentBranch = branchRes.status === 0 ? branchRes.stdout.trim() : '';
|
|
220
|
+
if (currentBranch === targetBranch) {
|
|
221
|
+
return currentBranch;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!gitRemoteBranchExists(targetBranch)) {
|
|
225
|
+
throw new Error(`Release channel branch "${targetBranch}" was not found on origin.`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (gitWorkingTreeDirty()) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Cannot switch to ${targetBranch} while the git worktree has local changes. Commit or stash them first, then rerun the update.`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (gitLocalBranchExists(targetBranch)) {
|
|
235
|
+
runOrThrow('git', ['checkout', targetBranch]);
|
|
236
|
+
} else {
|
|
237
|
+
runOrThrow('git', ['checkout', '-b', targetBranch, '--track', `origin/${targetBranch}`]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (currentBranch) {
|
|
241
|
+
logOk(`Switched git branch ${currentBranch} -> ${targetBranch}`);
|
|
242
|
+
} else {
|
|
243
|
+
logOk(`Checked out git branch ${targetBranch}`);
|
|
244
|
+
}
|
|
245
|
+
return targetBranch;
|
|
246
|
+
}
|
|
247
|
+
|
|
187
248
|
function ensureLogDir() {
|
|
188
249
|
ensureRuntimeDirs();
|
|
189
250
|
}
|
|
@@ -565,6 +626,7 @@ async function cmdStatus() {
|
|
|
565
626
|
heading(`${APP_NAME} Status`);
|
|
566
627
|
const port = loadEnvPort();
|
|
567
628
|
const running = await isPortOpen(port);
|
|
629
|
+
const releaseChannel = currentReleaseChannel();
|
|
568
630
|
|
|
569
631
|
if (running) {
|
|
570
632
|
logOk(`running on http://localhost:${port}`);
|
|
@@ -574,6 +636,7 @@ async function cmdStatus() {
|
|
|
574
636
|
|
|
575
637
|
console.log(` install root ${APP_DIR}`);
|
|
576
638
|
console.log(` version ${currentInstalledVersionLabel()}`);
|
|
639
|
+
console.log(` release channel ${releaseChannelSummary(releaseChannel)}`);
|
|
577
640
|
|
|
578
641
|
const processes = listNeoAgentServerProcesses();
|
|
579
642
|
if (processes.length > 0) {
|
|
@@ -595,23 +658,51 @@ function cmdLogs() {
|
|
|
595
658
|
runOrThrow('tail', ['-f', log, err], { cwd: APP_DIR });
|
|
596
659
|
}
|
|
597
660
|
|
|
598
|
-
function
|
|
661
|
+
function cmdChannel(args = []) {
|
|
662
|
+
heading('Release Channel');
|
|
663
|
+
const requested = args[0];
|
|
664
|
+
|
|
665
|
+
if (!requested) {
|
|
666
|
+
const channel = currentReleaseChannel();
|
|
667
|
+
console.log(` configured ${releaseChannelSummary(channel)}`);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const nextChannel = parseReleaseChannel(requested);
|
|
672
|
+
if (!nextChannel) {
|
|
673
|
+
throw new Error('Usage: neoagent channel [stable|beta]');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
writeReleaseChannelToEnvFile(nextChannel, ENV_FILE);
|
|
677
|
+
process.env.NEOAGENT_RELEASE_CHANNEL = nextChannel;
|
|
678
|
+
logOk(`Release channel set to ${releaseChannelSummary(nextChannel)}`);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function cmdUpdate(args = []) {
|
|
599
682
|
heading(`Update ${APP_NAME}`);
|
|
600
683
|
migrateLegacyRuntime((msg) => logInfo(msg));
|
|
601
684
|
ensureRuntimeDirs();
|
|
685
|
+
const requestedChannel = args[0] ? parseReleaseChannel(args[0]) : null;
|
|
686
|
+
if (args[0] && !requestedChannel) {
|
|
687
|
+
throw new Error('Usage: neoagent update [stable|beta]');
|
|
688
|
+
}
|
|
689
|
+
const releaseChannel = requestedChannel || currentReleaseChannel();
|
|
690
|
+
if (requestedChannel) {
|
|
691
|
+
writeReleaseChannelToEnvFile(releaseChannel, ENV_FILE);
|
|
692
|
+
process.env.NEOAGENT_RELEASE_CHANNEL = releaseChannel;
|
|
693
|
+
logOk(`Release channel set to ${releaseChannelSummary(releaseChannel)}`);
|
|
694
|
+
}
|
|
695
|
+
const targetBranch = getReleaseChannelBranch(releaseChannel);
|
|
696
|
+
const npmTag = getReleaseChannelDistTag(releaseChannel);
|
|
602
697
|
const versionBefore = currentInstalledVersionLabel();
|
|
603
698
|
let versionAfter = versionBefore;
|
|
604
699
|
|
|
605
700
|
if (fs.existsSync(path.join(APP_DIR, '.git')) && commandExists('git')) {
|
|
606
|
-
const branch = runQuiet('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
607
701
|
const current = runQuiet('git', ['rev-parse', '--short', 'HEAD']);
|
|
608
702
|
|
|
609
|
-
runOrThrow('git', ['fetch', 'origin']);
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
} else {
|
|
613
|
-
runOrThrow('git', ['pull', '--rebase', '--autostash']);
|
|
614
|
-
}
|
|
703
|
+
runOrThrow('git', ['fetch', 'origin', targetBranch]);
|
|
704
|
+
ensureGitBranchForReleaseChannel(targetBranch);
|
|
705
|
+
runOrThrow('git', ['pull', '--rebase', '--autostash', 'origin', targetBranch]);
|
|
615
706
|
|
|
616
707
|
const next = runQuiet('git', ['rev-parse', '--short', 'HEAD']);
|
|
617
708
|
if (current.status === 0 && next.status === 0 && current.stdout.trim() !== next.stdout.trim()) {
|
|
@@ -623,16 +714,16 @@ function cmdUpdate() {
|
|
|
623
714
|
buildBundledWebClientIfPossible();
|
|
624
715
|
}
|
|
625
716
|
} else {
|
|
626
|
-
logWarn(
|
|
717
|
+
logWarn(`No git repo detected; attempting npm global update from ${npmTag}.`);
|
|
627
718
|
if (commandExists('npm')) {
|
|
628
719
|
try {
|
|
629
720
|
backupRuntimeData();
|
|
630
|
-
runOrThrow('npm', ['install', '-g',
|
|
721
|
+
runOrThrow('npm', ['install', '-g', `neoagent@${npmTag}`, '--force'], {
|
|
631
722
|
env: withInstallEnv()
|
|
632
723
|
});
|
|
633
724
|
logOk('npm global update completed (forced reinstall)');
|
|
634
725
|
} catch {
|
|
635
|
-
logWarn(
|
|
726
|
+
logWarn(`npm global update failed. Run: npm install -g neoagent@${npmTag} --force`);
|
|
636
727
|
}
|
|
637
728
|
} else {
|
|
638
729
|
logWarn('npm not found. Cannot perform global update.');
|
|
@@ -702,7 +793,9 @@ async function cmdEnv(args = []) {
|
|
|
702
793
|
function printHelp() {
|
|
703
794
|
console.log(`${APP_NAME} manager`);
|
|
704
795
|
console.log('Usage: neoagent <command>');
|
|
705
|
-
console.log('Commands: install | setup | env | update | restart | start | stop | status | logs | uninstall');
|
|
796
|
+
console.log('Commands: install | setup | env | channel | update | restart | start | stop | status | logs | uninstall');
|
|
797
|
+
console.log('Channel usage: neoagent channel | neoagent channel stable | neoagent channel beta');
|
|
798
|
+
console.log('Update usage: neoagent update | neoagent update stable | neoagent update beta');
|
|
706
799
|
console.log('Env usage: neoagent env list | neoagent env get PORT | neoagent env set PORT 3333 | neoagent env unset PORT');
|
|
707
800
|
}
|
|
708
801
|
|
|
@@ -721,8 +814,11 @@ async function runCLI(argv) {
|
|
|
721
814
|
case 'env':
|
|
722
815
|
await cmdEnv(argv.slice(1));
|
|
723
816
|
break;
|
|
817
|
+
case 'channel':
|
|
818
|
+
cmdChannel(argv.slice(1));
|
|
819
|
+
break;
|
|
724
820
|
case 'update':
|
|
725
|
-
cmdUpdate();
|
|
821
|
+
cmdUpdate(argv.slice(1));
|
|
726
822
|
break;
|
|
727
823
|
case 'restart':
|
|
728
824
|
cmdRestart();
|
package/package.json
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { ENV_FILE } = require('./paths');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_RELEASE_CHANNEL = 'stable';
|
|
8
|
+
const RELEASE_CHANNEL_ENV_KEY = 'NEOAGENT_RELEASE_CHANNEL';
|
|
9
|
+
const RELEASE_CHANNEL_BRANCHES = Object.freeze({
|
|
10
|
+
stable: 'main',
|
|
11
|
+
beta: 'beta',
|
|
12
|
+
});
|
|
13
|
+
const RELEASE_CHANNEL_DIST_TAGS = Object.freeze({
|
|
14
|
+
stable: 'latest',
|
|
15
|
+
beta: 'beta',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function parseEnv(raw) {
|
|
19
|
+
const map = new Map();
|
|
20
|
+
for (const line of String(raw || '').split('\n')) {
|
|
21
|
+
if (!line || line.startsWith('#') || !line.includes('=')) continue;
|
|
22
|
+
const idx = line.indexOf('=');
|
|
23
|
+
const key = line.slice(0, idx).trim();
|
|
24
|
+
const value = line.slice(idx + 1);
|
|
25
|
+
if (key) map.set(key, value);
|
|
26
|
+
}
|
|
27
|
+
return map;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseReleaseChannel(value) {
|
|
31
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
32
|
+
switch (normalized) {
|
|
33
|
+
case 'stable':
|
|
34
|
+
case 'normal':
|
|
35
|
+
case 'default':
|
|
36
|
+
case 'latest':
|
|
37
|
+
case 'main':
|
|
38
|
+
return 'stable';
|
|
39
|
+
case 'beta':
|
|
40
|
+
case 'preview':
|
|
41
|
+
case 'prerelease':
|
|
42
|
+
case 'pre-release':
|
|
43
|
+
return 'beta';
|
|
44
|
+
default:
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeReleaseChannel(value) {
|
|
50
|
+
return parseReleaseChannel(value) || DEFAULT_RELEASE_CHANNEL;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getReleaseChannelBranch(channel) {
|
|
54
|
+
return RELEASE_CHANNEL_BRANCHES[normalizeReleaseChannel(channel)];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getReleaseChannelDistTag(channel) {
|
|
58
|
+
return RELEASE_CHANNEL_DIST_TAGS[normalizeReleaseChannel(channel)];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getReleaseChannelLabel(channel) {
|
|
62
|
+
return normalizeReleaseChannel(channel) === 'beta' ? 'Beta' : 'Stable';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readReleaseChannelFromRaw(raw) {
|
|
66
|
+
const env = parseEnv(raw);
|
|
67
|
+
return normalizeReleaseChannel(env.get(RELEASE_CHANNEL_ENV_KEY));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readReleaseChannelFromEnvFile(envFile = ENV_FILE) {
|
|
71
|
+
try {
|
|
72
|
+
return readReleaseChannelFromRaw(fs.readFileSync(envFile, 'utf8'));
|
|
73
|
+
} catch {
|
|
74
|
+
return DEFAULT_RELEASE_CHANNEL;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readConfiguredReleaseChannel({ env = process.env, envFile = ENV_FILE } = {}) {
|
|
79
|
+
return normalizeReleaseChannel(env[RELEASE_CHANNEL_ENV_KEY] || readReleaseChannelFromEnvFile(envFile));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeReleaseChannelToEnvFile(channel, envFile = ENV_FILE) {
|
|
83
|
+
const normalized = parseReleaseChannel(channel);
|
|
84
|
+
if (!normalized) {
|
|
85
|
+
throw new Error('Release channel must be "stable" or "beta".');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const raw = fs.existsSync(envFile) ? fs.readFileSync(envFile, 'utf8') : '';
|
|
89
|
+
const lines = raw ? raw.split('\n') : [];
|
|
90
|
+
let replaced = false;
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < lines.length; i++) {
|
|
93
|
+
if (lines[i].startsWith(`${RELEASE_CHANNEL_ENV_KEY}=`)) {
|
|
94
|
+
lines[i] = `${RELEASE_CHANNEL_ENV_KEY}=${normalized}`;
|
|
95
|
+
replaced = true;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!replaced) {
|
|
101
|
+
lines.push(`${RELEASE_CHANNEL_ENV_KEY}=${normalized}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const output =
|
|
105
|
+
lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
|
|
106
|
+
fs.mkdirSync(path.dirname(envFile), { recursive: true });
|
|
107
|
+
fs.writeFileSync(envFile, output, { mode: 0o600 });
|
|
108
|
+
return normalized;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
DEFAULT_RELEASE_CHANNEL,
|
|
113
|
+
RELEASE_CHANNEL_ENV_KEY,
|
|
114
|
+
parseReleaseChannel,
|
|
115
|
+
normalizeReleaseChannel,
|
|
116
|
+
getReleaseChannelBranch,
|
|
117
|
+
getReleaseChannelDistTag,
|
|
118
|
+
getReleaseChannelLabel,
|
|
119
|
+
readReleaseChannelFromRaw,
|
|
120
|
+
readReleaseChannelFromEnvFile,
|
|
121
|
+
readConfiguredReleaseChannel,
|
|
122
|
+
writeReleaseChannelToEnvFile,
|
|
123
|
+
};
|
package/server/index.js
CHANGED
|
@@ -53,6 +53,7 @@ registerStaticRoutes(app);
|
|
|
53
53
|
registerErrorHandler(app);
|
|
54
54
|
|
|
55
55
|
let shuttingDown = false;
|
|
56
|
+
let shutdownExitCode = 0;
|
|
56
57
|
|
|
57
58
|
httpServer.on('connection', (socket) => {
|
|
58
59
|
activeSockets.add(socket);
|
|
@@ -146,7 +147,8 @@ function closeHttpServer(server, sockets, timeoutMs = 5000) {
|
|
|
146
147
|
});
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
async function shutdown() {
|
|
150
|
+
async function shutdown(exitCode = 0) {
|
|
151
|
+
shutdownExitCode = Math.max(shutdownExitCode, exitCode);
|
|
150
152
|
if (shuttingDown) return;
|
|
151
153
|
shuttingDown = true;
|
|
152
154
|
|
|
@@ -159,12 +161,15 @@ async function shutdown() {
|
|
|
159
161
|
]);
|
|
160
162
|
|
|
161
163
|
db.close();
|
|
162
|
-
process.exit(
|
|
164
|
+
process.exit(shutdownExitCode);
|
|
163
165
|
}
|
|
164
166
|
|
|
165
|
-
httpServer.listen(PORT,
|
|
167
|
+
httpServer.listen(PORT, () => {
|
|
166
168
|
console.log(`NeoAgent running on http://localhost:${PORT}`);
|
|
167
|
-
|
|
169
|
+
startServices(app, io).catch(async (err) => {
|
|
170
|
+
console.error('[Startup] Service initialization failed:', err);
|
|
171
|
+
await shutdown(1);
|
|
172
|
+
});
|
|
168
173
|
});
|
|
169
174
|
|
|
170
175
|
process.on('SIGINT', shutdown);
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"052f31d115eceda8cbff1b3481fcde4330c4ae
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "1964138140" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|