@stratusagent/cli 0.9.0 → 0.9.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/README.md +52 -0
- package/dist/index.d.ts +24 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +374 -6
- package/dist/index.js.map +1 -1
- package/dist/service.d.ts +34 -2
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +104 -5
- package/dist/service.js.map +1 -1
- package/package.json +21 -21
package/dist/index.js
CHANGED
|
@@ -18,15 +18,67 @@ import { loadPlugins } from '@stratusagent/plugins';
|
|
|
18
18
|
import { createFileCommandWhitelist, createPermissionPolicy, describeCommandScope, } from '@stratusagent/permissions';
|
|
19
19
|
import { DEFAULT_ANTHROPIC_MODEL, redactAnthropicRawTurns, } from '@stratusagent/provider-anthropic';
|
|
20
20
|
import { canonicalDestination, createForgetTool, createRecallTool, createRememberTool, defineAgent, describeCadence, describeSchedule, FORGET_TOOL_NAME, MEMORY_TOOL_NAME, formatSoul, generateAgentName, parseSoul, } from '@stratusagent/agents';
|
|
21
|
-
import { agentsDirPath, claimSoulFile, collectAvailableModels as collectModels, createDemoTool, createFileMemoryStore, createRuntimeProvider, credentialsPath, defaultApiKeyEnvName, DEFAULT_ANTHROPIC_BASE_URL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_STRATUS_AGENT, discoverActiveConfig, globalConfigPath, loadChannelCredentials, loadCredentials, installSkillsFromDirectory, loadOperatorSkills, loadRosterSouls, skillsDirPath, listAgentSummaries, loadSoulFile, memoryFilePath, migrateLegacyMemory, parseProviderName, readNonEmptyString, readProcessEnv, resolveConfiguredSoul, logsDirPath, readWorkingDirectory, resolveAgentApprovals, DEFAULT_CONFIG_FILENAME, loadConfigFile, resolveConfigLocation, resolveRuntimeConfig as resolveStateRuntimeConfig, saveChannelCredentials, saveConfigFile, readTrustedConfigBlock, saveCredentials, servedRuntimes, verifyProviderKey, withLegacyDefaultMemories, workspacesDirPath, gatewayInfoPath, gatewayTokenPath, } from '@stratusagent/state';
|
|
22
|
-
import { installService, readServiceStatus, servicePlatform, serviceUnitPath, startService, stopService, uninstallService, } from "./service.js";
|
|
23
|
-
export { installService, launchdPlist, readServiceStatus, servicePlatform, serviceUnitPath, startService, stopService, systemdUnit, uninstallService, SERVICE_LABEL, } from "./service.js";
|
|
21
|
+
import { agentsDirPath, claimSoulFile, collectAvailableModels as collectModels, createDemoTool, createFileMemoryStore, createRuntimeProvider, credentialsPath, defaultApiKeyEnvName, DEFAULT_ANTHROPIC_BASE_URL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_MODEL, DEFAULT_STRATUS_AGENT, discoverActiveConfig, globalConfigPath, loadChannelCredentials, loadCredentials, installSkillsFromDirectory, loadOperatorSkills, loadRosterSouls, skillsDirPath, listAgentSummaries, loadSoulFile, memoryFilePath, migrateLegacyMemory, parseProviderName, readNonEmptyString, readProcessEnv, resolveConfiguredSoul, logsDirPath, readWorkingDirectory, resolveAgentApprovals, DEFAULT_CONFIG_FILENAME, loadConfigFile, resolveConfigLocation, resolveRuntimeConfig as resolveStateRuntimeConfig, saveChannelCredentials, saveConfigFile, readTrustedConfigBlock, saveCredentials, servedRuntimes, verifyProviderKey, withLegacyDefaultMemories, workspacesDirPath, gatewayInfoPath, gatewayTokenPath, newerStateMessage, pendingStateMigrations, readStateStamp, runStateMigrations, STATE_SCHEMA_VERSION, } from '@stratusagent/state';
|
|
22
|
+
import { installService, readServiceCommand, readServiceStatus, servicePlatform, serviceUnitPath, startService, stopService, uninstallService, } from "./service.js";
|
|
23
|
+
export { installService, launchdPlist, readServiceCommand, readServiceStatus, servicePlatform, serviceUnitPath, startService, stopService, systemdUnit, uninstallService, SERVICE_LABEL, } from "./service.js";
|
|
24
24
|
import { createLogWriter, currentLogPosition, truncateRedirectLogs, formatLogRecord, readRecentRecords, tailLog, } from "./logs.js";
|
|
25
25
|
export { createLogWriter, currentLogPosition, truncateRedirectLogs, formatLogRecord, parseLogLines, readRecentRecords, tailLog, } from "./logs.js";
|
|
26
26
|
// The shared state package owns config resolution, credentials, memory, and
|
|
27
27
|
// provider wiring now (the gateway uses the same code); the CLI re-exports
|
|
28
28
|
// its historical surface so existing importers keep working.
|
|
29
29
|
export { createFileMemoryStore } from '@stratusagent/state';
|
|
30
|
+
/** How long `stratus update` waits for npm to answer a version lookup. */
|
|
31
|
+
export const VERSION_LOOKUP_TIMEOUT_MS = 15_000;
|
|
32
|
+
const defaultPackageVersionFetcher = async (packageName) => {
|
|
33
|
+
const { spawn } = await import('node:child_process');
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const child = spawn('npm', ['view', packageName, 'version'], {
|
|
36
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
37
|
+
shell: npmNeedsShell(process.platform),
|
|
38
|
+
});
|
|
39
|
+
let stdout = '';
|
|
40
|
+
let settled = false;
|
|
41
|
+
const finish = (value) => {
|
|
42
|
+
if (settled) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
settled = true;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
resolve(value);
|
|
48
|
+
};
|
|
49
|
+
// An unreachable registry must degrade to "unknown", never hang the
|
|
50
|
+
// update on someone's terminal.
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
child.kill('SIGKILL');
|
|
53
|
+
finish(undefined);
|
|
54
|
+
}, VERSION_LOOKUP_TIMEOUT_MS);
|
|
55
|
+
timer.unref?.();
|
|
56
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
57
|
+
child.once('error', () => finish(undefined));
|
|
58
|
+
child.once('close', (code) => finish(code === 0 && stdout.trim().length > 0 ? stdout.trim() : undefined));
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Dotted-numeric comparison, enough for this package's own versions:
|
|
63
|
+
* positive when `a` is newer than `b`. Anything unparseable in a segment
|
|
64
|
+
* counts as zero rather than throwing — a weird registry answer must not
|
|
65
|
+
* crash the update that would fix things.
|
|
66
|
+
*/
|
|
67
|
+
export const compareVersions = (a, b) => {
|
|
68
|
+
const parse = (value) => value.trim().replace(/^v/, '').split('.').map((part) => {
|
|
69
|
+
const numeric = Number.parseInt(part, 10);
|
|
70
|
+
return Number.isInteger(numeric) ? numeric : 0;
|
|
71
|
+
});
|
|
72
|
+
const left = parse(a);
|
|
73
|
+
const right = parse(b);
|
|
74
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
75
|
+
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
76
|
+
if (difference !== 0) {
|
|
77
|
+
return difference;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
};
|
|
30
82
|
/**
|
|
31
83
|
* Whether spawning npm needs a shell on this platform.
|
|
32
84
|
*
|
|
@@ -70,7 +122,7 @@ const defaultPackageInstaller = async (packages) => {
|
|
|
70
122
|
});
|
|
71
123
|
});
|
|
72
124
|
};
|
|
73
|
-
export const CLI_VERSION = '0.9.
|
|
125
|
+
export const CLI_VERSION = '0.9.1';
|
|
74
126
|
const DASHBOARD_TITLE = 'Stratus Agent Dashboard';
|
|
75
127
|
/**
|
|
76
128
|
* The range every package declares in its `engines` field. Two floors, not
|
|
@@ -130,6 +182,8 @@ Usage:
|
|
|
130
182
|
stratus schedules
|
|
131
183
|
stratus schedules cancel <id>
|
|
132
184
|
stratus doctor
|
|
185
|
+
stratus update
|
|
186
|
+
stratus update --check
|
|
133
187
|
stratus service install
|
|
134
188
|
stratus service status
|
|
135
189
|
stratus logs -f
|
|
@@ -190,6 +244,14 @@ Commands:
|
|
|
190
244
|
doctor Show what a run would use right now — provider, model, soul —
|
|
191
245
|
and which file or environment variable decided each, then
|
|
192
246
|
flag anything that would surprise you (--format json)
|
|
247
|
+
update The whole upgrade dance, in the order that cannot lose
|
|
248
|
+
data: stop stratusd, upgrade the package from npm, run
|
|
249
|
+
pending state migrations, rewrite the service unit with
|
|
250
|
+
current node/entrypoint paths, restart. --check reports
|
|
251
|
+
what it would do without doing any of it (exits 1 when
|
|
252
|
+
something is actionable). Works offline too — an
|
|
253
|
+
unreachable npm skips the upgrade but still migrates and
|
|
254
|
+
repairs the unit
|
|
193
255
|
dashboard Open the web dashboard: finds a running daemon (or starts one),
|
|
194
256
|
mints a single-use sign-in link, and opens your browser at it.
|
|
195
257
|
Needs @stratusagent/control-api and @stratusagent/dashboard
|
|
@@ -564,6 +626,23 @@ export const parseCommand = (argv, env = {}) => {
|
|
|
564
626
|
}
|
|
565
627
|
return { command: 'doctor', format, ...(configPath ? { configPath } : {}) };
|
|
566
628
|
}
|
|
629
|
+
if (command === 'update') {
|
|
630
|
+
let check = false;
|
|
631
|
+
for (const token of rest) {
|
|
632
|
+
if (!token) {
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (token === '--help' || token === '-h') {
|
|
636
|
+
return { command: 'help' };
|
|
637
|
+
}
|
|
638
|
+
if (token === '--check') {
|
|
639
|
+
check = true;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
throw new Error(`Unknown option: ${token}`);
|
|
643
|
+
}
|
|
644
|
+
return { command: 'update', check };
|
|
645
|
+
}
|
|
567
646
|
if (command === 'service') {
|
|
568
647
|
const action = rest[0];
|
|
569
648
|
if (action === '--help' || action === '-h' || action === undefined) {
|
|
@@ -3500,6 +3579,19 @@ export const collectDoctorReport = async (command, warn, env = {}) => {
|
|
|
3500
3579
|
problems.push(`Slack tokens are stored for ${orphans.join(', ')}, which no agent matches — \`stratus serve\` skips them. `
|
|
3501
3580
|
+ 'Clear them from `stratus setup` → Channels.');
|
|
3502
3581
|
}
|
|
3582
|
+
// The invisible failure doctor exists to surface: the service unit embeds
|
|
3583
|
+
// absolute node and entrypoint paths, and upgrading node (an nvm version
|
|
3584
|
+
// directory, say) leaves the unit pointing at an interpreter that no
|
|
3585
|
+
// longer exists. The service stops working and nothing else says so.
|
|
3586
|
+
const unitCommand = await readServiceCommand(serviceEnvFor(env));
|
|
3587
|
+
if (unitCommand?.execPath !== undefined && !(await pathExists(unitCommand.execPath))) {
|
|
3588
|
+
problems.push(`The service unit points at a node interpreter that no longer exists (${unitCommand.execPath}), so stratusd cannot start. `
|
|
3589
|
+
+ 'Run `stratus update` (or `stratus service install`) to rewrite it with current paths.');
|
|
3590
|
+
}
|
|
3591
|
+
if (unitCommand?.scriptPath !== undefined && !(await pathExists(unitCommand.scriptPath))) {
|
|
3592
|
+
problems.push(`The service unit points at a CLI entrypoint that no longer exists (${unitCommand.scriptPath}), so stratusd cannot start. `
|
|
3593
|
+
+ 'Run `stratus update` (or `stratus service install`) to rewrite it with current paths.');
|
|
3594
|
+
}
|
|
3503
3595
|
return {
|
|
3504
3596
|
...(winner ? { configInUse: winner.path } : {}),
|
|
3505
3597
|
configShadowed: shadowed.map((entry) => entry.path),
|
|
@@ -3638,6 +3730,7 @@ const serviceEnvFor = (env) => ({
|
|
|
3638
3730
|
...(env.homeDir !== undefined ? { homeDir: env.homeDir } : {}),
|
|
3639
3731
|
cwd: readWorkingDirectory(env),
|
|
3640
3732
|
...(env.serviceRunner !== undefined ? { run: env.serviceRunner } : {}),
|
|
3733
|
+
...(env.servicePlatform !== undefined ? { platform: env.servicePlatform } : {}),
|
|
3641
3734
|
});
|
|
3642
3735
|
/**
|
|
3643
3736
|
* A credential that exists only in this shell cannot reach the daemon: a
|
|
@@ -3720,7 +3813,9 @@ export const runService = async (command, streams, env = {}) => {
|
|
|
3720
3813
|
writeLine(streams.stdout, `No service manager for ${process.platform}. Run \`stratus serve\` yourself.`);
|
|
3721
3814
|
return 1;
|
|
3722
3815
|
}
|
|
3723
|
-
writeLine(streams.stdout, `stratusd ${status.running
|
|
3816
|
+
writeLine(streams.stdout, `stratusd ${status.running === undefined
|
|
3817
|
+
? 'state unknown — the service manager did not answer'
|
|
3818
|
+
: status.running ? 'running' : status.installed ? 'installed, not running' : 'not installed'}`);
|
|
3724
3819
|
writeLine(streams.stdout, ` manager ${status.platform}`);
|
|
3725
3820
|
writeLine(streams.stdout, ` unit ${status.unitPath}`);
|
|
3726
3821
|
if (status.detail) {
|
|
@@ -3734,7 +3829,7 @@ export const runService = async (command, streams, env = {}) => {
|
|
|
3734
3829
|
writeLine(streams.stdout, status.installed
|
|
3735
3830
|
? ' logs stratus logs -f'
|
|
3736
3831
|
: ' install stratus service install');
|
|
3737
|
-
return status.running ? 0 : 1;
|
|
3832
|
+
return status.running === true ? 0 : 1;
|
|
3738
3833
|
}
|
|
3739
3834
|
// A service manager passes none of this shell's environment on, so a
|
|
3740
3835
|
// config selected by STRATUS_CONFIG has to be baked into the unit
|
|
@@ -3796,6 +3891,227 @@ export const runService = async (command, streams, env = {}) => {
|
|
|
3796
3891
|
}
|
|
3797
3892
|
return result.ok ? 0 : 1;
|
|
3798
3893
|
};
|
|
3894
|
+
const CLI_PACKAGE_NAME = '@stratusagent/cli';
|
|
3895
|
+
const pathExists = async (candidate) => {
|
|
3896
|
+
try {
|
|
3897
|
+
await stat(candidate);
|
|
3898
|
+
return true;
|
|
3899
|
+
}
|
|
3900
|
+
catch {
|
|
3901
|
+
return false;
|
|
3902
|
+
}
|
|
3903
|
+
};
|
|
3904
|
+
/**
|
|
3905
|
+
* `stratus update` — the whole upgrade dance, in the order that cannot lose
|
|
3906
|
+
* data: stop the service (so no daemon holds the session database while
|
|
3907
|
+
* state migrates), upgrade the package, run pending migrations, rewrite the
|
|
3908
|
+
* service unit with current paths, restart.
|
|
3909
|
+
*
|
|
3910
|
+
* The unit rewrite is the step that repairs the failure nothing else
|
|
3911
|
+
* surfaces: the unit embeds absolute node and entrypoint paths (a service
|
|
3912
|
+
* manager loads no shell profile, so it must), and upgrading node — under
|
|
3913
|
+
* nvm, a whole new version directory — leaves the unit pointing at an
|
|
3914
|
+
* interpreter that no longer exists. The service stops working and nothing
|
|
3915
|
+
* says so; the agents just stop answering.
|
|
3916
|
+
*
|
|
3917
|
+
* Each step degrades independently: an unreachable npm skips the version
|
|
3918
|
+
* check and package upgrade but still migrates and rewrites the unit —
|
|
3919
|
+
* which is exactly the repair the offline case needs.
|
|
3920
|
+
*/
|
|
3921
|
+
const runUpdate = async (command, streams, env) => {
|
|
3922
|
+
const serviceEnv = serviceEnvFor(env);
|
|
3923
|
+
const out = (line) => writeLine(streams.stdout, line);
|
|
3924
|
+
const latest = await (env.packageVersionFetcher ?? defaultPackageVersionFetcher)(CLI_PACKAGE_NAME);
|
|
3925
|
+
const upgradeAvailable = latest !== undefined && compareVersions(latest, CLI_VERSION) > 0;
|
|
3926
|
+
// A stamp from a newer build makes every later step wrong, not just the
|
|
3927
|
+
// migration one: this build would migrate — and stop the daemon to do it —
|
|
3928
|
+
// against a format it does not understand. Checked here, before anything
|
|
3929
|
+
// has a side effect.
|
|
3930
|
+
const stamp = await readStateStamp(env);
|
|
3931
|
+
const stateNewer = stamp.schemaVersion > STATE_SCHEMA_VERSION;
|
|
3932
|
+
const status = await readServiceStatus(serviceEnv).catch(() => undefined);
|
|
3933
|
+
const unit = status?.installed ? await readServiceCommand(serviceEnv) : undefined;
|
|
3934
|
+
const unitNotes = [];
|
|
3935
|
+
if (status?.installed && unit?.execPath !== undefined) {
|
|
3936
|
+
if (!(await pathExists(unit.execPath))) {
|
|
3937
|
+
unitNotes.push(`the unit's interpreter no longer exists: ${unit.execPath} — stratusd cannot start until the unit is rewritten`);
|
|
3938
|
+
}
|
|
3939
|
+
else if (unit.execPath !== process.execPath) {
|
|
3940
|
+
unitNotes.push(`the unit runs ${unit.execPath}; this shell runs ${process.execPath}`);
|
|
3941
|
+
}
|
|
3942
|
+
if (unit.scriptPath !== undefined && !(await pathExists(unit.scriptPath))) {
|
|
3943
|
+
unitNotes.push(`the unit's entrypoint no longer exists: ${unit.scriptPath}`);
|
|
3944
|
+
}
|
|
3945
|
+
else if (unit.scriptPath !== undefined && process.argv[1] !== undefined && unit.scriptPath !== process.argv[1]) {
|
|
3946
|
+
// A versioned package directory can keep the old file alive after an
|
|
3947
|
+
// upgrade — the daemon then runs the old CLI indefinitely with
|
|
3948
|
+
// nothing missing on disk to notice.
|
|
3949
|
+
unitNotes.push(`the unit runs entrypoint ${unit.scriptPath}; this shell runs ${process.argv[1]}`);
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
const pending = await pendingStateMigrations(env);
|
|
3953
|
+
out(`stratus ${CLI_VERSION}`);
|
|
3954
|
+
out(latest === undefined
|
|
3955
|
+
? ' latest unknown — npm did not answer'
|
|
3956
|
+
: ` latest ${latest}${upgradeAvailable ? ' — update available' : ' — up to date'}`);
|
|
3957
|
+
out(` state schema ${stamp.schemaVersion}${stateNewer
|
|
3958
|
+
? ` — written by a NEWER build than this one (which understands ${STATE_SCHEMA_VERSION})`
|
|
3959
|
+
: `, ${pending.length === 0 ? 'no pending migrations' : `${pending.length} pending migration${pending.length === 1 ? '' : 's'}`}`}`);
|
|
3960
|
+
out(` service ${status === undefined
|
|
3961
|
+
? `no service manager for ${process.platform}`
|
|
3962
|
+
: status.installed
|
|
3963
|
+
? (status.running === undefined ? 'installed, state unknown' : status.running ? 'installed, running' : 'installed, not running')
|
|
3964
|
+
: 'not installed'}`);
|
|
3965
|
+
for (const note of unitNotes) {
|
|
3966
|
+
out(` unit ${note}`);
|
|
3967
|
+
}
|
|
3968
|
+
if (command.check) {
|
|
3969
|
+
for (const migration of pending) {
|
|
3970
|
+
out(` pending ${migration.id} — ${migration.description}`);
|
|
3971
|
+
}
|
|
3972
|
+
if (stateNewer) {
|
|
3973
|
+
out('This build cannot update that state — upgrade the package itself (`npm install -g @stratusagent/cli`).');
|
|
3974
|
+
return 1;
|
|
3975
|
+
}
|
|
3976
|
+
const actionable = upgradeAvailable || pending.length > 0 || unitNotes.length > 0;
|
|
3977
|
+
out(actionable
|
|
3978
|
+
? 'Run `stratus update` to apply the above.'
|
|
3979
|
+
: 'Nothing to do.');
|
|
3980
|
+
// Actionable exits 1, so a cron job or script can notice.
|
|
3981
|
+
return actionable ? 1 : 0;
|
|
3982
|
+
}
|
|
3983
|
+
if (stateNewer) {
|
|
3984
|
+
writeLine(streams.stderr, newerStateMessage(stamp.schemaVersion));
|
|
3985
|
+
return 1;
|
|
3986
|
+
}
|
|
3987
|
+
if (status?.installed && (status.runAtLogin === undefined || status.running === undefined)) {
|
|
3988
|
+
// The rewrite has to re-state the login setting and restore the prior
|
|
3989
|
+
// run state, and the manager could not say what either currently is.
|
|
3990
|
+
// Guessing would let a transient status failure convert a deliberate
|
|
3991
|
+
// --no-login install, or rewrite-and-stop a daemon that was actually
|
|
3992
|
+
// running — refuse instead, before anything has been stopped.
|
|
3993
|
+
writeLine(streams.stderr, `Not updating: whether stratusd ${status.running === undefined ? 'is running' : 'starts at login'} could not be determined (the service manager did not answer), and the unit rewrite would have to guess. Check \`stratus service status\` and retry.`);
|
|
3994
|
+
return 1;
|
|
3995
|
+
}
|
|
3996
|
+
const wasRunning = status?.running === true;
|
|
3997
|
+
if (wasRunning) {
|
|
3998
|
+
// No daemon may hold the session database while state migrates — this
|
|
3999
|
+
// bracket is the one place an update could otherwise lose data.
|
|
4000
|
+
out('Stopping stratusd for the update…');
|
|
4001
|
+
const stopped = await stopService(serviceEnv);
|
|
4002
|
+
for (const message of stopped.messages) {
|
|
4003
|
+
writeLine(stopped.ok ? streams.stdout : streams.stderr, message);
|
|
4004
|
+
}
|
|
4005
|
+
if (!stopped.ok) {
|
|
4006
|
+
writeLine(streams.stderr, 'Not updating while the daemon may still be running.');
|
|
4007
|
+
return 1;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
let upgradeFailed = false;
|
|
4011
|
+
if (upgradeAvailable) {
|
|
4012
|
+
out(`Upgrading ${CLI_PACKAGE_NAME} ${CLI_VERSION} → ${latest}…`);
|
|
4013
|
+
const installed = await (env.packageInstaller ?? defaultPackageInstaller)([`${CLI_PACKAGE_NAME}@latest`]);
|
|
4014
|
+
if (installed.ok) {
|
|
4015
|
+
// This process is still the old build; migrations the new version
|
|
4016
|
+
// adds run when it first starts — which the restart below is.
|
|
4017
|
+
out('Upgraded. Migrations the new version adds run on its first start.');
|
|
4018
|
+
}
|
|
4019
|
+
else {
|
|
4020
|
+
upgradeFailed = true;
|
|
4021
|
+
writeLine(streams.stderr, `npm install failed: ${installed.message || 'unknown error'} — continuing with migrations and the unit rewrite.`);
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
else {
|
|
4025
|
+
out(latest === undefined
|
|
4026
|
+
? 'Skipping the package upgrade — npm did not answer.'
|
|
4027
|
+
: 'Package already up to date.');
|
|
4028
|
+
}
|
|
4029
|
+
let applied;
|
|
4030
|
+
try {
|
|
4031
|
+
applied = await runStateMigrations(env);
|
|
4032
|
+
}
|
|
4033
|
+
catch (error) {
|
|
4034
|
+
// A daemon stopped for an update that then failed must not stay down:
|
|
4035
|
+
// the old unit is still in place (the rewrite has not happened), so
|
|
4036
|
+
// restarting restores the world the update found. The failure is still
|
|
4037
|
+
// a failure — but an offline fleet on top of it is not.
|
|
4038
|
+
writeLine(streams.stderr, `State migration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4039
|
+
if (wasRunning) {
|
|
4040
|
+
const restarted = await startService(serviceEnv);
|
|
4041
|
+
for (const message of restarted.messages) {
|
|
4042
|
+
writeLine(restarted.ok ? streams.stdout : streams.stderr, message);
|
|
4043
|
+
}
|
|
4044
|
+
writeLine(streams.stderr, restarted.ok
|
|
4045
|
+
? 'stratusd was restarted on its previous unit. Fix the migration failure and run `stratus update` again.'
|
|
4046
|
+
: 'stratusd could not be restarted either — bring it back with `stratus service start` once the failure is fixed.');
|
|
4047
|
+
}
|
|
4048
|
+
return 1;
|
|
4049
|
+
}
|
|
4050
|
+
if (applied.length === 0) {
|
|
4051
|
+
out('No pending state migrations.');
|
|
4052
|
+
}
|
|
4053
|
+
for (const migration of applied) {
|
|
4054
|
+
out(`Migrated: ${migration.description}${migration.detail !== undefined ? ` — ${migration.detail}` : ''}`);
|
|
4055
|
+
}
|
|
4056
|
+
if (status?.installed) {
|
|
4057
|
+
out('Rewriting the service unit with current node and entrypoint paths…');
|
|
4058
|
+
// The rewrite overwrites the unit before it bootstraps, so a failure
|
|
4059
|
+
// in between must be able to put the old definition back — otherwise
|
|
4060
|
+
// a stopped daemon is left with neither unit to start from.
|
|
4061
|
+
const previousUnit = await readFile(serviceUnitPath(serviceEnv), 'utf8').catch(() => undefined);
|
|
4062
|
+
const install = await installService(
|
|
4063
|
+
// The unit's own working directory survives the rewrite: relative
|
|
4064
|
+
// paths in the pinned config (a `soul`) resolve against it, so a
|
|
4065
|
+
// rewrite run from some other directory must not substitute its own.
|
|
4066
|
+
unit?.workingDirectory !== undefined ? { ...serviceEnv, cwd: unit.workingDirectory } : serviceEnv, {
|
|
4067
|
+
...(status.runAtLogin === false ? { runAtLogin: false } : {}),
|
|
4068
|
+
// The config the existing unit was pinned to survives the rewrite —
|
|
4069
|
+
// a unit rewritten onto a different roster is its own outage.
|
|
4070
|
+
...(unit?.configPath !== undefined ? { configPath: unit.configPath } : {}),
|
|
4071
|
+
});
|
|
4072
|
+
for (const message of install.messages) {
|
|
4073
|
+
writeLine(install.ok ? streams.stdout : streams.stderr, message);
|
|
4074
|
+
}
|
|
4075
|
+
if (!install.ok) {
|
|
4076
|
+
if (wasRunning) {
|
|
4077
|
+
// The fleet was up when the update began and must not stay down
|
|
4078
|
+
// over a failed rewrite: restore the previous unit definition and
|
|
4079
|
+
// start it again. The exit code stays 1 — the update failed — but
|
|
4080
|
+
// the world it found is put back.
|
|
4081
|
+
if (previousUnit !== undefined) {
|
|
4082
|
+
await writeFile(serviceUnitPath(serviceEnv), previousUnit, { mode: 0o644 }).catch((error) => {
|
|
4083
|
+
writeLine(streams.stderr, `Could not restore the previous unit either: ${error instanceof Error ? error.message : String(error)}`);
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
const restarted = await startService(serviceEnv);
|
|
4087
|
+
for (const message of restarted.messages) {
|
|
4088
|
+
writeLine(restarted.ok ? streams.stdout : streams.stderr, message);
|
|
4089
|
+
}
|
|
4090
|
+
writeLine(streams.stderr, restarted.ok
|
|
4091
|
+
? 'The unit rewrite failed, so stratusd was restarted on its previous unit. Fix the failure above and run `stratus update` again.'
|
|
4092
|
+
: 'The unit rewrite failed AND stratusd could not be restarted — bring it back with `stratus service start`, or `stratus service install` to rewrite the unit by hand.');
|
|
4093
|
+
}
|
|
4094
|
+
return 1;
|
|
4095
|
+
}
|
|
4096
|
+
if (!wasRunning) {
|
|
4097
|
+
// installService starts the daemon; a service that was deliberately
|
|
4098
|
+
// stopped before the update stays that way.
|
|
4099
|
+
const stopped = await stopService(serviceEnv);
|
|
4100
|
+
if (!stopped.ok) {
|
|
4101
|
+
// The update itself succeeded, but the user's prior state was not
|
|
4102
|
+
// restored: an intentionally stopped daemon is now running. That
|
|
4103
|
+
// is a failure a script must see, not a footnote.
|
|
4104
|
+
writeLine(streams.stderr, 'stratusd was not running before the update, and stopping it again failed — it is now RUNNING. Stop it with `stratus service stop`.');
|
|
4105
|
+
return 1;
|
|
4106
|
+
}
|
|
4107
|
+
out('stratusd was not running before the update, so it was left stopped.');
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
else if (status !== undefined) {
|
|
4111
|
+
out('No service installed — nothing to rewrite. `stratus service install` sets one up.');
|
|
4112
|
+
}
|
|
4113
|
+
return upgradeFailed ? 1 : 0;
|
|
4114
|
+
};
|
|
3799
4115
|
/**
|
|
3800
4116
|
* What a `skill add` source string means, in order: a directory on this
|
|
3801
4117
|
* machine, a GitHub `owner/repo` shorthand, or a URL git can clone. The
|
|
@@ -4840,6 +5156,55 @@ export const runCli = async ({ argv, streams = process, env = {} }) => {
|
|
|
4840
5156
|
writeLine(streams.stdout, HELP_TEXT);
|
|
4841
5157
|
return 0;
|
|
4842
5158
|
}
|
|
5159
|
+
// Migrations run on first use of a newer build — every command, every
|
|
5160
|
+
// install path — not only via `stratus update`: state that migrates
|
|
5161
|
+
// only sometimes is worse than state that never migrates, because the
|
|
5162
|
+
// two populations diverge silently. `update` is excluded because it
|
|
5163
|
+
// owns the migration step at its own place in the upgrade sequence
|
|
5164
|
+
// (after the service stop and the package upgrade), and `--check` has
|
|
5165
|
+
// to be able to report what is pending rather than having just done it.
|
|
5166
|
+
if (command.command !== 'update') {
|
|
5167
|
+
const stamp = await readStateStamp(resolvedEnv);
|
|
5168
|
+
if (stamp.schemaVersion > STATE_SCHEMA_VERSION) {
|
|
5169
|
+
// Anything that writes under ~/.stratus refuses, not only the
|
|
5170
|
+
// daemon: a downgraded build's setup, chat, or run can discard
|
|
5171
|
+
// fields and invariants the newer format relies on — the exact
|
|
5172
|
+
// hazard the stamp exists to close. Read-only commands warn and
|
|
5173
|
+
// continue, because reading logs or the roster is how someone
|
|
5174
|
+
// diagnoses their way OUT of this state; so do `service stop`,
|
|
5175
|
+
// `status`, and `uninstall`, for the same reason. (`agent new`
|
|
5176
|
+
// only prints an identity — it writes nothing.)
|
|
5177
|
+
const writesState = command.command === 'serve'
|
|
5178
|
+
|| command.command === 'setup'
|
|
5179
|
+
|| command.command === 'chat'
|
|
5180
|
+
|| command.command === 'run'
|
|
5181
|
+
|| command.command === 'skill-add'
|
|
5182
|
+
|| command.command === 'dashboard'
|
|
5183
|
+
|| (command.command === 'schedules' && command.action === 'cancel')
|
|
5184
|
+
|| (command.command === 'service' && (command.action === 'install' || command.action === 'start'));
|
|
5185
|
+
if (writesState) {
|
|
5186
|
+
writeLine(streams.stderr, newerStateMessage(stamp.schemaVersion));
|
|
5187
|
+
writeLine(streams.stderr, `Refusing \`stratus ${command.command}\` — it writes state the newer format owns. Read-only commands (logs, agents, doctor, service status/stop) still work.`);
|
|
5188
|
+
return 1;
|
|
5189
|
+
}
|
|
5190
|
+
writeLine(streams.stderr, `Warning: ${newerStateMessage(stamp.schemaVersion)}`);
|
|
5191
|
+
}
|
|
5192
|
+
else {
|
|
5193
|
+
try {
|
|
5194
|
+
for (const migration of await runStateMigrations(resolvedEnv)) {
|
|
5195
|
+
if (migration.detail !== undefined) {
|
|
5196
|
+
writeLine(streams.stderr, `state migration ${migration.id}: ${migration.detail}`);
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
catch (error) {
|
|
5201
|
+
// A failed migration must not brick every command, but running
|
|
5202
|
+
// on unmigrated state is worth a line: silence here is how the
|
|
5203
|
+
// migrated and unmigrated populations diverge.
|
|
5204
|
+
writeLine(streams.stderr, `Warning: state migration failed (${error instanceof Error ? error.message : String(error)}). Continuing on unmigrated state — \`stratus update\` retries it.`);
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
4843
5208
|
// Every handler is awaited, never bare-returned: a bare `return
|
|
4844
5209
|
// promise` inside try/catch settles the async function before the
|
|
4845
5210
|
// catch can see it, so a command that fails at runtime — a gateway
|
|
@@ -4869,6 +5234,9 @@ export const runCli = async ({ argv, streams = process, env = {} }) => {
|
|
|
4869
5234
|
if (command.command === 'service') {
|
|
4870
5235
|
return await runService(command, streams, resolvedEnv);
|
|
4871
5236
|
}
|
|
5237
|
+
if (command.command === 'update') {
|
|
5238
|
+
return await runUpdate(command, streams, resolvedEnv);
|
|
5239
|
+
}
|
|
4872
5240
|
if (command.command === 'chat') {
|
|
4873
5241
|
return await runChat(command, streams, resolvedEnv);
|
|
4874
5242
|
}
|