@skrr-ai/cli 0.1.94 → 0.1.95
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 +153 -165
- package/bin/run.js +32 -0
- package/bin/update-npm-runner.cjs +33 -0
- package/bin/update-session.cjs +194 -0
- package/bin/update-worker.cjs +208 -0
- package/dist/base-command.js +0 -15
- package/dist/commands/code/doctor.d.ts +1 -0
- package/dist/commands/code/doctor.js +6 -0
- package/dist/commands/code/index.js +4 -0
- package/dist/commands/code/install.d.ts +5 -62
- package/dist/commands/code/install.js +86 -279
- package/dist/commands/code/rollback.d.ts +8 -0
- package/dist/commands/code/rollback.js +20 -0
- package/dist/commands/code/update.d.ts +8 -37
- package/dist/commands/code/update.js +134 -213
- package/dist/commands/daemon/install.js +4 -0
- package/dist/commands/generate-recovery-code.d.ts +13 -1
- package/dist/commands/generate-recovery-code.js +146 -13
- package/dist/commands/instructions/guide.d.ts +20 -0
- package/dist/commands/instructions/guide.js +84 -0
- package/dist/commands/instructions/install.js +57 -33
- package/dist/commands/instructions/setup.d.ts +14 -0
- package/dist/commands/instructions/setup.js +72 -0
- package/dist/commands/integrations/execute.js +1 -1
- package/dist/commands/login.d.ts +5 -6
- package/dist/commands/login.js +27 -11
- package/dist/commands/logout.js +7 -7
- package/dist/commands/security/overview.d.ts +11 -0
- package/dist/commands/security/overview.js +158 -0
- package/dist/commands/spaces/claim.d.ts +12 -0
- package/dist/commands/spaces/claim.js +42 -0
- package/dist/commands/spaces/ownership.d.ts +12 -0
- package/dist/commands/spaces/ownership.js +46 -0
- package/dist/commands/tasks/cancel-cascade.js +1 -1
- package/dist/commands/tasks/create-tree.d.ts +5 -1
- package/dist/commands/tasks/create-tree.js +35 -7
- package/dist/commands/tasks/interrupt.js +2 -2
- package/dist/commands/tasks/pause-schedule.js +1 -1
- package/dist/commands/token/list.d.ts +3 -5
- package/dist/commands/token/list.js +99 -57
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.js +61 -0
- package/dist/generated/managed-instruction-guides.json +9 -0
- package/dist/hooks/init.d.ts +5 -0
- package/dist/hooks/init.js +27 -0
- package/dist/lib/commitments.d.ts +0 -8
- package/dist/lib/commitments.js +0 -52
- package/dist/lib/daemon-installer.d.ts +2 -0
- package/dist/lib/daemon-installer.js +100 -24
- package/dist/lib/first-party-harness-doctor.js +31 -1
- package/dist/lib/first-party-harness-remote-install.d.ts +71 -0
- package/dist/lib/first-party-harness-remote-install.js +310 -0
- package/dist/lib/first-party-harness-remote-update.d.ts +46 -0
- package/dist/lib/first-party-harness-remote-update.js +242 -0
- package/dist/lib/first-party-harness-startup-update.d.ts +26 -0
- package/dist/lib/first-party-harness-startup-update.js +166 -0
- package/dist/lib/first-party-harness-update.d.ts +46 -0
- package/dist/lib/first-party-harness-update.js +289 -0
- package/dist/lib/first-party-harness.js +7 -4
- package/dist/lib/managed-instruction-local.d.ts +12 -0
- package/dist/lib/managed-instruction-local.js +58 -0
- package/dist/lib/managed-instruction-setup.d.ts +35 -0
- package/dist/lib/managed-instruction-setup.js +306 -0
- package/dist/lib/memory-sync.d.ts +1 -0
- package/dist/lib/memory-sync.js +279 -68
- package/dist/lib/platform-update-local.d.ts +2 -0
- package/dist/lib/platform-update-local.js +113 -0
- package/dist/lib/platform-update.d.ts +39 -0
- package/dist/lib/platform-update.js +84 -0
- package/dist/lib/profile-machine-identity-guard.d.ts +2 -0
- package/dist/lib/profile-machine-identity-guard.js +13 -0
- package/dist/lib/prompt.d.ts +1 -0
- package/dist/lib/prompt.js +4 -3
- package/dist/lib/task-batch.d.ts +9 -0
- package/dist/lib/task-batch.js +94 -17
- package/dist/lib/task-instruction-offer.d.ts +1 -4
- package/dist/lib/task-instruction-offer.js +3 -129
- package/dist/lib/update-check.d.ts +61 -53
- package/dist/lib/update-check.js +453 -314
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +3 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +3 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessReleaseKeys.d.ts +82 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessReleaseKeys.js +105 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessUpdater.d.ts +110 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessUpdater.js +945 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessPermissionControls.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessPermissionControls.js +62 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +35 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releasePolicy.d.ts +63 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releasePolicy.js +169 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseVersions.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseVersions.js +44 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +3 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +3 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessReleaseKeys.d.ts +82 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessReleaseKeys.js +102 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessUpdater.d.ts +110 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessUpdater.js +923 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessPermissionControls.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessPermissionControls.js +59 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +35 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releasePolicy.d.ts +63 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releasePolicy.js +163 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseVersions.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseVersions.js +41 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +31 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +6584 -6567
- package/docs/v1/README.md +28 -0
- package/docs/v1/api.md +55 -0
- package/docs/v1/changelog.md +16 -0
- package/docs/v1/check-examples.mjs +178 -0
- package/docs/v1/examples/api-readonly.mjs +144 -0
- package/docs/v1/operations.md +87 -0
- package/docs/v1/quickstart.md +71 -0
- package/oclif.manifest.json +46007 -45462
- package/package.json +13 -11
package/dist/lib/update-check.js
CHANGED
|
@@ -37,91 +37,46 @@ exports.normalizeBaseUrl = normalizeBaseUrl;
|
|
|
37
37
|
exports.isNudgeSuppressed = isNudgeSuppressed;
|
|
38
38
|
exports.isAutoUpdateSuppressed = isAutoUpdateSuppressed;
|
|
39
39
|
exports.isOlderVersion = isOlderVersion;
|
|
40
|
+
exports.isPrivateCliPrefix = isPrivateCliPrefix;
|
|
40
41
|
exports.resolveCliPackageDir = resolveCliPackageDir;
|
|
41
42
|
exports.classifyCliInstall = classifyCliInstall;
|
|
43
|
+
exports.cliUpdateCommand = cliUpdateCommand;
|
|
42
44
|
exports.buildNudgeLine = buildNudgeLine;
|
|
43
45
|
exports.decideCliUpdateAction = decideCliUpdateAction;
|
|
46
|
+
exports.readCliUpdateStatus = readCliUpdateStatus;
|
|
47
|
+
exports.checkCliUpdate = checkCliUpdate;
|
|
48
|
+
exports.performCliUpdate = performCliUpdate;
|
|
44
49
|
exports.maybeWarnOutdatedCli = maybeWarnOutdatedCli;
|
|
50
|
+
exports.shouldCheckCliUpdateAtStartup = shouldCheckCliUpdateAtStartup;
|
|
45
51
|
const node_child_process_1 = require("node:child_process");
|
|
46
52
|
const fs = __importStar(require("node:fs"));
|
|
47
53
|
const os = __importStar(require("node:os"));
|
|
48
54
|
const path = __importStar(require("node:path"));
|
|
55
|
+
const node_crypto_1 = require("node:crypto");
|
|
49
56
|
const node_util_1 = require("node:util");
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
* of its own — the bytes belong to whatever installed them. On every command
|
|
56
|
-
* this module decides — from a ~4h on-disk cache, with ZERO added latency —
|
|
57
|
-
* what to do about version state, and refreshes that cache in the background:
|
|
58
|
-
*
|
|
59
|
-
* - npm-global, writable install + outdated → AUTO-UPDATE: schedule a
|
|
60
|
-
* detached `npm install -g @skrr-ai/cli@<latest>` that runs after this
|
|
61
|
-
* command exits (so a mid-flight command never reads half-swapped files),
|
|
62
|
-
* once per TTL window, and print one stderr line saying so. If a later
|
|
63
|
-
* run still sees the old version the attempt failed and we fall back to
|
|
64
|
-
* the manual nudge.
|
|
65
|
-
* - every other install shape → NUDGE: a single stderr line telling the
|
|
66
|
-
* user the right update command for THEIR install (a project-local dep
|
|
67
|
-
* is updated inside its project; a dev checkout is rebuilt, not npm'd).
|
|
68
|
-
* - installs we must not touch → silence: the app-bundled CLI updates with
|
|
69
|
-
* the desktop app; image-baked installs (dedicated-runtime guest) are
|
|
70
|
-
* owned by the image.
|
|
71
|
-
*
|
|
72
|
-
* Hard guarantees (a version check must never make the CLI worse):
|
|
73
|
-
* - Writes ONLY to stderr, never stdout, so `--json` output stays clean.
|
|
74
|
-
* - Prints at most ONE line, and only on an interactive TTY — scripts, CI
|
|
75
|
-
* and pipes neither see the nudge nor trigger an auto-update.
|
|
76
|
-
* - Never blocks the command: the decision is synchronous off the cache;
|
|
77
|
-
* the network refresh and install classification are fire-and-forget
|
|
78
|
-
* and time-bounded; the actual `npm install` is deferred to process
|
|
79
|
-
* exit and detached.
|
|
80
|
-
* - Never throws: every path is wrapped so a malformed cache, offline
|
|
81
|
-
* API, missing npm, or read-only home directory can't break a command.
|
|
82
|
-
* - Never escalates: a global prefix we cannot write stays a nudge; the
|
|
83
|
-
* CLI will not reach for sudo.
|
|
84
|
-
* - Suppressible: `OVERSKY_NO_UPDATE_NUDGE` / `OVERSKY_SKIP_UPDATE_CHECK`
|
|
85
|
-
* silence everything; `OVERSKY_CLI_AUTO_UPDATE=0` keeps the nudge but
|
|
86
|
-
* skips the background install; `OVERSKY_CLI_BUNDLED=1` (set by the
|
|
87
|
-
* desktop app's `skrr` wrapper) suppresses it entirely.
|
|
88
|
-
*/
|
|
89
|
-
/** How long a cached advisory stays fresh before a background refresh. */
|
|
90
|
-
const CHECK_TTL_MS = 4 * 60 * 60 * 1000; // 4h
|
|
91
|
-
/** Hard ceiling on the background refresh request. */
|
|
57
|
+
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
58
|
+
/** One update owner and one startup advisory. npm continues to own npm bytes;
|
|
59
|
+
* the durable worker waits for cooperating CLI sessions before replacing them.
|
|
60
|
+
* All update decisions are observable, and checking never schedules a change. */
|
|
61
|
+
const CHECK_TTL_MS = 4 * 60 * 60 * 1000;
|
|
92
62
|
const FETCH_TIMEOUT_MS = 1500;
|
|
93
|
-
/** Hard ceiling on the `npm root -g` classification probe. */
|
|
94
63
|
const NPM_ROOT_TIMEOUT_MS = 1500;
|
|
95
|
-
|
|
96
|
-
* image-pinned and must never be told to `npm update -g`. */
|
|
64
|
+
const CLI_PACKAGE = '@skrr-ai/cli';
|
|
97
65
|
const DEDICATED_RUNTIME_ENV_FILE = '/etc/skrr-dedicated-runtime.env';
|
|
98
|
-
|
|
99
|
-
const HOSTED_RUNTIME_SOURCES = new Set([
|
|
100
|
-
'dedicated-runtime',
|
|
101
|
-
'hosted-machine',
|
|
102
|
-
'cloud-coding',
|
|
103
|
-
]);
|
|
66
|
+
const HOSTED_RUNTIME_SOURCES = new Set(['dedicated-runtime', 'hosted-machine', 'cloud-coding']);
|
|
104
67
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
105
|
-
function cacheFilePath() {
|
|
106
|
-
return path.join(
|
|
68
|
+
function cacheFilePath(env = process.env) {
|
|
69
|
+
return path.join((0, auth_core_1.resolveConfigRoot)(env), 'cli-update-check.json');
|
|
107
70
|
}
|
|
108
|
-
/** Trailing-slash- and case-insensitive normalization for baseURL compares. */
|
|
109
71
|
function normalizeBaseUrl(url) {
|
|
110
72
|
return url.trim().replace(/\/+$/, '').toLowerCase();
|
|
111
73
|
}
|
|
112
74
|
function isTruthyEnv(value) {
|
|
113
|
-
|
|
114
|
-
return false;
|
|
115
|
-
return ['1', 'true', 'on', 'yes'].includes(value.trim().toLowerCase());
|
|
75
|
+
return !!value && ['1', 'true', 'on', 'yes'].includes(value.trim().toLowerCase());
|
|
116
76
|
}
|
|
117
77
|
function isDisabledEnv(value) {
|
|
118
|
-
|
|
119
|
-
return false;
|
|
120
|
-
return ['0', 'false', 'off', 'no', 'disabled'].includes(value.trim().toLowerCase());
|
|
78
|
+
return !!value && ['0', 'false', 'off', 'no', 'disabled'].includes(value.trim().toLowerCase());
|
|
121
79
|
}
|
|
122
|
-
/** True when this process runs on an image-pinned hosted-runtime guest. The
|
|
123
|
-
* env marker covers the daemon unit's context; the env FILE is the reliable
|
|
124
|
-
* signal for interactive tenant shells that never inherit it. */
|
|
125
80
|
function isHostedRuntimeGuest(env) {
|
|
126
81
|
if (HOSTED_RUNTIME_SOURCES.has((env.OVERSKY_DAEMON_SOURCE ?? '').trim()))
|
|
127
82
|
return true;
|
|
@@ -132,67 +87,44 @@ function isHostedRuntimeGuest(env) {
|
|
|
132
87
|
return false;
|
|
133
88
|
}
|
|
134
89
|
}
|
|
135
|
-
/**
|
|
136
|
-
* True when the advisory must stay silent regardless of version state:
|
|
137
|
-
* explicit suppression, a desktop-app-bundled CLI (which auto-updates with
|
|
138
|
-
* the app and must not be told to `npm update -g`), or an image-baked install
|
|
139
|
-
* the control plane owns.
|
|
140
|
-
*/
|
|
141
90
|
function isNudgeSuppressed(env = process.env) {
|
|
142
91
|
return (isTruthyEnv(env.OVERSKY_NO_UPDATE_NUDGE) ||
|
|
143
92
|
isTruthyEnv(env.OVERSKY_SKIP_UPDATE_CHECK) ||
|
|
93
|
+
isTruthyEnv(env.OCLIF_SKIP_NEW_VERSION_CHECK) ||
|
|
144
94
|
env.OVERSKY_CLI_BUNDLED === '1' ||
|
|
145
95
|
isHostedRuntimeGuest(env));
|
|
146
96
|
}
|
|
147
|
-
/** True when the background self-update is allowed to run at all. Suppression
|
|
148
|
-
* implies no auto-update — a user who asked for silence did not ask for a
|
|
149
|
-
* silent mutation of their global npm. */
|
|
150
97
|
function isAutoUpdateSuppressed(env = process.env) {
|
|
151
|
-
return isNudgeSuppressed(env) ||
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
* Parse the numeric release core of a version (`1.2.3-rc.1` → `[1,2,3]`),
|
|
155
|
-
* padded to three parts. Returns null when the core isn't cleanly numeric,
|
|
156
|
-
* so an unparseable version can only ever DISABLE the nudge, never fire a
|
|
157
|
-
* spurious one.
|
|
158
|
-
*/
|
|
159
|
-
function parseVersionCore(version) {
|
|
160
|
-
const core = version.trim().split(/[-+]/, 1)[0];
|
|
161
|
-
const parts = core.split('.');
|
|
162
|
-
if (parts.length === 0 || parts.length > 3)
|
|
163
|
-
return null;
|
|
164
|
-
const nums = [];
|
|
165
|
-
for (const part of parts) {
|
|
166
|
-
if (!/^\d+$/.test(part))
|
|
167
|
-
return null;
|
|
168
|
-
nums.push(Number(part));
|
|
169
|
-
}
|
|
170
|
-
while (nums.length < 3)
|
|
171
|
-
nums.push(0);
|
|
172
|
-
return [nums[0], nums[1], nums[2]];
|
|
98
|
+
return (isNudgeSuppressed(env) ||
|
|
99
|
+
isDisabledEnv(env.OVERSKY_CLI_AUTO_UPDATE) ||
|
|
100
|
+
env.SKRR_CLI_UPDATE_SESSION_UNAVAILABLE === '1');
|
|
173
101
|
}
|
|
174
|
-
/**
|
|
175
|
-
* True iff `current` is strictly older than `latest` by numeric release
|
|
176
|
-
* core. Conservative: any unparseable input returns false (no nudge).
|
|
177
|
-
* Prerelease suffixes are ignored, so `0.2.0-rc.1` is treated as `0.2.0` —
|
|
178
|
-
* we never nag someone testing a prerelease of the latest line.
|
|
179
|
-
*/
|
|
180
102
|
function isOlderVersion(current, latest) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
103
|
+
return (0, auth_core_1.compareReleaseVersions)(current, latest) === -1;
|
|
104
|
+
}
|
|
105
|
+
/** Per-user leases cannot coordinate a global prefix shared with another OS
|
|
106
|
+
* user. Only private user-owned prefixes participate in managed replacement. */
|
|
107
|
+
function isPrivateCliPrefix(root) {
|
|
108
|
+
if (process.platform === 'win32' || typeof process.getuid !== 'function')
|
|
184
109
|
return false;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (
|
|
110
|
+
try {
|
|
111
|
+
const home = fs.realpathSync(os.homedir());
|
|
112
|
+
let directory = fs.realpathSync(root);
|
|
113
|
+
if (!directory.startsWith(`${home}${path.sep}`))
|
|
189
114
|
return false;
|
|
115
|
+
while (directory !== home) {
|
|
116
|
+
const stat = fs.statSync(directory);
|
|
117
|
+
if (stat.uid !== process.getuid() || (stat.mode & 0o022) !== 0)
|
|
118
|
+
return false;
|
|
119
|
+
directory = path.dirname(directory);
|
|
120
|
+
}
|
|
121
|
+
const homeStat = fs.statSync(home);
|
|
122
|
+
return homeStat.uid === process.getuid() && (homeStat.mode & 0o022) === 0;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
190
126
|
}
|
|
191
|
-
return false;
|
|
192
127
|
}
|
|
193
|
-
// ---------------------------------------------------------------------------
|
|
194
|
-
// Install-shape classification
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
128
|
const CLI_PACKAGE_DIR_RE = /^(.*[/\\]node_modules[/\\]@skrr-ai[/\\]cli)[/\\]/;
|
|
197
129
|
/**
|
|
198
130
|
* Resolve the package directory this process was launched from when it lives
|
|
@@ -240,11 +172,22 @@ function dirWritable(dir) {
|
|
|
240
172
|
* dir beside `dist` exists only in a checkout — published packages ship
|
|
241
173
|
* `dist` and no `src`.
|
|
242
174
|
*/
|
|
243
|
-
function classifyCliInstall(env, argv, npmGlobalRoot) {
|
|
244
|
-
if (
|
|
175
|
+
function classifyCliInstall(env, argv, npmGlobalRoot, probeWritable = true) {
|
|
176
|
+
if (isHostedRuntimeGuest(env))
|
|
177
|
+
return { kind: 'image' };
|
|
178
|
+
if (env.OVERSKY_CLI_BUNDLED === '1')
|
|
245
179
|
return { kind: 'bundled' };
|
|
180
|
+
const entry = argv[1];
|
|
181
|
+
try {
|
|
182
|
+
if (entry && /[/\\]Cellar[/\\]/.test(fs.realpathSync(entry)))
|
|
183
|
+
return { kind: 'homebrew' };
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
/* an unavailable entry remains conservatively classified */
|
|
246
187
|
}
|
|
247
188
|
const packageDir = resolveCliPackageDir(argv);
|
|
189
|
+
if (packageDir && fs.existsSync(path.join(packageDir, 'src', 'commands')))
|
|
190
|
+
return { kind: 'dev' };
|
|
248
191
|
if (!packageDir) {
|
|
249
192
|
// A dev checkout launches `cli/bin/run.js` directly — a `src/commands`
|
|
250
193
|
// sibling of `dist` exists only in a checkout (published packages ship
|
|
@@ -290,7 +233,19 @@ function classifyCliInstall(env, argv, npmGlobalRoot) {
|
|
|
290
233
|
// proof we must not treat it as ours to auto-update.
|
|
291
234
|
return { kind: 'npm-local' };
|
|
292
235
|
}
|
|
293
|
-
|
|
236
|
+
let writable = false;
|
|
237
|
+
if (probeWritable)
|
|
238
|
+
writable = dirWritable(nodeModulesDir);
|
|
239
|
+
else {
|
|
240
|
+
try {
|
|
241
|
+
fs.accessSync(nodeModulesDir, fs.constants.W_OK);
|
|
242
|
+
writable = true;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
/* read-only */
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return { kind: 'npm-global', writable };
|
|
294
249
|
}
|
|
295
250
|
/**
|
|
296
251
|
* Resolve `npm root -g` for install classification. Runs ONLY inside the
|
|
@@ -298,11 +253,12 @@ function classifyCliInstall(env, argv, npmGlobalRoot) {
|
|
|
298
253
|
* no npm, a timeout, or an odd answer just leaves classification to the
|
|
299
254
|
* path shape alone.
|
|
300
255
|
*/
|
|
301
|
-
async function resolveNpmGlobalRoot() {
|
|
256
|
+
async function resolveNpmGlobalRoot(env = process.env) {
|
|
302
257
|
try {
|
|
303
258
|
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
304
259
|
const { stdout } = await execFileAsync(npmBin, ['root', '-g'], {
|
|
305
260
|
timeout: NPM_ROOT_TIMEOUT_MS,
|
|
261
|
+
env,
|
|
306
262
|
maxBuffer: 64 * 1024,
|
|
307
263
|
shell: process.platform === 'win32',
|
|
308
264
|
});
|
|
@@ -313,253 +269,436 @@ async function resolveNpmGlobalRoot() {
|
|
|
313
269
|
return null;
|
|
314
270
|
}
|
|
315
271
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
* Build the one-line stderr advisory, or null when no nudge is warranted.
|
|
321
|
-
* `installKind` tailors the command to the bytes' owner: a project-local dep
|
|
322
|
-
* is updated inside its project, a dev checkout is rebuilt — telling either
|
|
323
|
-
* to `npm update -g` would fix the wrong install.
|
|
324
|
-
*/
|
|
325
|
-
function buildNudgeLine(currentVersion, latest, npmPackage, installKind) {
|
|
326
|
-
if (!latest || !isOlderVersion(currentVersion, latest))
|
|
327
|
-
return null;
|
|
328
|
-
const pkg = npmPackage || '@skrr-ai/cli';
|
|
329
|
-
const prefix = `[skrr] Update available: ${currentVersion} → ${latest}. `;
|
|
330
|
-
const suffix = ` Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`;
|
|
331
|
-
switch (installKind) {
|
|
272
|
+
function cliUpdateCommand(owner) {
|
|
273
|
+
switch (owner) {
|
|
274
|
+
case 'npm-global':
|
|
275
|
+
return `npm install -g ${CLI_PACKAGE}@latest`;
|
|
332
276
|
case 'npm-local':
|
|
333
|
-
return
|
|
277
|
+
return `npm update ${CLI_PACKAGE}`;
|
|
334
278
|
case 'dev':
|
|
335
|
-
return
|
|
336
|
-
case 'other':
|
|
337
|
-
return `${prefix}Update via your install method — see https://skrr.ai.${suffix}`;
|
|
338
|
-
case 'npm-global':
|
|
279
|
+
return 'npm --prefix cli run build';
|
|
339
280
|
case 'bundled':
|
|
281
|
+
return 'Update the skrr desktop app';
|
|
282
|
+
case 'image':
|
|
283
|
+
return 'Update the managed runtime image';
|
|
284
|
+
case 'homebrew':
|
|
285
|
+
return 'brew upgrade skrr';
|
|
340
286
|
default:
|
|
341
|
-
return
|
|
287
|
+
return 'Update via your install method — see https://skrr.ai';
|
|
342
288
|
}
|
|
343
289
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
290
|
+
function buildNudgeLine(currentVersion, latest, _npmPackage, installKind = 'npm-global') {
|
|
291
|
+
if (!latest || !isOlderVersion(currentVersion, latest))
|
|
292
|
+
return null;
|
|
293
|
+
const command = installKind === 'npm-global' ? `npm update -g ${CLI_PACKAGE}` : cliUpdateCommand(installKind);
|
|
294
|
+
const context = installKind === 'npm-local'
|
|
295
|
+
? ' in the owning project'
|
|
296
|
+
: installKind === 'dev'
|
|
297
|
+
? ' to rebuild your checkout'
|
|
298
|
+
: '';
|
|
299
|
+
return `[skrr] Update available: ${currentVersion} → ${latest}. Run \`${command}\`${context}. Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`;
|
|
300
|
+
}
|
|
353
301
|
function decideCliUpdateAction(args) {
|
|
354
302
|
const { currentVersion, cache, env, isTTY } = args;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
303
|
+
if (!isTTY ||
|
|
304
|
+
isNudgeSuppressed(env) ||
|
|
305
|
+
!cache?.latest ||
|
|
306
|
+
!isOlderVersion(currentVersion, cache.latest))
|
|
359
307
|
return null;
|
|
360
308
|
const install = cache.install ?? { kind: 'other' };
|
|
361
|
-
const pkg = cache.npmPackage || '@skrr-ai/cli';
|
|
362
309
|
const attempt = cache.autoUpdate;
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
310
|
+
const sameAttempt = attempt?.version === cache.latest;
|
|
311
|
+
const fresh = sameAttempt && (args.now ?? Date.now()) - attempt.attemptedAt < CHECK_TTL_MS;
|
|
312
|
+
if (sameAttempt &&
|
|
313
|
+
((!attempt.status && fresh) || attempt.status === 'queued' || attempt.status === 'running')) {
|
|
314
|
+
return {
|
|
315
|
+
type: 'nudge',
|
|
316
|
+
line: `[skrr] CLI update to ${cache.latest} ${attempt.status === 'running' ? 'is running' : 'is queued until active CLI sessions exit'}. Inspect \`skrr update --status\`.\n`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (install.kind === 'npm-global' && install.writable && !isAutoUpdateSuppressed(env)) {
|
|
320
|
+
if (!fresh)
|
|
366
321
|
return {
|
|
367
322
|
type: 'auto-update',
|
|
368
323
|
version: cache.latest,
|
|
369
|
-
pkg,
|
|
370
|
-
line: `[skrr]
|
|
371
|
-
`(\`npm install -g ${pkg}@${cache.latest}\`). Disable with OVERSKY_CLI_AUTO_UPDATE=0.\n`,
|
|
324
|
+
pkg: CLI_PACKAGE,
|
|
325
|
+
line: `[skrr] CLI update ${currentVersion} → ${cache.latest} queued in the background until active CLI sessions exit. Disable with OVERSKY_CLI_AUTO_UPDATE=0.\n`,
|
|
372
326
|
};
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
327
|
+
if (attempt?.status === 'failed')
|
|
328
|
+
return {
|
|
329
|
+
type: 'nudge',
|
|
330
|
+
line: `[skrr] CLI update to ${cache.latest} failed. Inspect \`skrr update --status\` or run \`${cliUpdateCommand(install.kind)}\`.\n`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const line = buildNudgeLine(currentVersion, cache.latest, CLI_PACKAGE, install.kind);
|
|
334
|
+
return line ? { type: 'nudge', line } : null;
|
|
335
|
+
}
|
|
336
|
+
function readCache(env = process.env) {
|
|
337
|
+
try {
|
|
338
|
+
const parsed = JSON.parse(fs.readFileSync(cacheFilePath(env), 'utf8'));
|
|
339
|
+
if (typeof parsed.lastCheckAt !== 'number' || typeof parsed.baseURL !== 'string')
|
|
340
|
+
return null;
|
|
376
341
|
return {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
` Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`,
|
|
342
|
+
...parsed,
|
|
343
|
+
latest: typeof parsed.latest === 'string' ? parsed.latest : null,
|
|
344
|
+
npmPackage: CLI_PACKAGE,
|
|
381
345
|
};
|
|
382
346
|
}
|
|
383
|
-
|
|
384
|
-
|
|
347
|
+
catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
385
350
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
351
|
+
function writeCache(cache, env) {
|
|
352
|
+
const file = cacheFilePath(env);
|
|
353
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
354
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
390
355
|
try {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
: undefined,
|
|
407
|
-
autoUpdate: parsed.autoUpdate &&
|
|
408
|
-
typeof parsed.autoUpdate.attemptedAt === 'number' &&
|
|
409
|
-
typeof parsed.autoUpdate.version === 'string'
|
|
410
|
-
? { attemptedAt: parsed.autoUpdate.attemptedAt, version: parsed.autoUpdate.version }
|
|
411
|
-
: undefined,
|
|
412
|
-
};
|
|
413
|
-
}
|
|
356
|
+
fs.writeFileSync(temporary, JSON.stringify(cache), { mode: 0o600 });
|
|
357
|
+
fs.renameSync(temporary, file);
|
|
358
|
+
}
|
|
359
|
+
finally {
|
|
360
|
+
fs.rmSync(temporary, { force: true });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function packageRoot(argv) {
|
|
364
|
+
const installed = resolveCliPackageDir(argv);
|
|
365
|
+
if (installed)
|
|
366
|
+
return installed;
|
|
367
|
+
try {
|
|
368
|
+
if (argv[1])
|
|
369
|
+
return path.resolve(path.dirname(fs.realpathSync(argv[1])), '..');
|
|
414
370
|
}
|
|
415
371
|
catch {
|
|
416
|
-
|
|
372
|
+
/* tests and virtual invocations */
|
|
417
373
|
}
|
|
418
|
-
return
|
|
374
|
+
return path.resolve(__dirname, '..', '..');
|
|
375
|
+
}
|
|
376
|
+
function runtime() {
|
|
377
|
+
// The standalone CommonJS bootstrap is also copied outside npm's package tree.
|
|
378
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
379
|
+
return require(path.resolve(__dirname, '..', '..', 'bin', 'update-session.cjs'));
|
|
419
380
|
}
|
|
420
|
-
function
|
|
381
|
+
function attemptFor(argv) {
|
|
421
382
|
try {
|
|
422
|
-
|
|
423
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
424
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
425
|
-
fs.writeFileSync(tmp, JSON.stringify(cache), { mode: 0o600 });
|
|
426
|
-
fs.renameSync(tmp, file);
|
|
383
|
+
return runtime().readJson(path.join(runtime().runtimeDir(packageRoot(argv)), 'result.json'));
|
|
427
384
|
}
|
|
428
385
|
catch {
|
|
429
|
-
|
|
386
|
+
return null;
|
|
430
387
|
}
|
|
431
388
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
389
|
+
function installedCliVersion(argv, fallback) {
|
|
390
|
+
try {
|
|
391
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot(argv), 'package.json'), 'utf8'));
|
|
392
|
+
return pkg.name === CLI_PACKAGE && typeof pkg.version === 'string' ? pkg.version : fallback;
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return resolveCliPackageDir(argv) ? null : fallback;
|
|
396
|
+
}
|
|
438
397
|
}
|
|
439
|
-
|
|
398
|
+
/** Cached, read-only status: no network, child process, lease, or mutation. */
|
|
399
|
+
function readCliUpdateStatus(opts) {
|
|
400
|
+
const env = opts.env ?? process.env;
|
|
401
|
+
const argv = opts.argv ?? process.argv;
|
|
402
|
+
const cache = readCache(env);
|
|
403
|
+
const matchingCache = cache?.baseURL === normalizeBaseUrl(opts.baseURL) ? cache : null;
|
|
404
|
+
// Classify the actual executing installation each time; a persisted owner
|
|
405
|
+
// from another installation/profile must never authorize npm mutation.
|
|
406
|
+
const install = classifyCliInstall(env, argv, matchingCache?.packageRoot === packageRoot(argv) ? (matchingCache.npmGlobalRoot ?? null) : null, false);
|
|
407
|
+
const installedVersion = installedCliVersion(argv, opts.currentVersion);
|
|
408
|
+
const offeredVersion = matchingCache?.latest ?? null;
|
|
409
|
+
let lastAttempt = attemptFor(argv);
|
|
410
|
+
if (lastAttempt?.status === 'running' && lastAttempt.workerPid) {
|
|
411
|
+
const api = runtime();
|
|
412
|
+
const mutation = api.readJson(path.join(api.runtimeDir(packageRoot(argv)), 'mutation.json'));
|
|
413
|
+
// Only kill(pid, 0): cached status remains free of subprocess/network work.
|
|
414
|
+
const activeWorker = api.isAlive({ pid: lastAttempt.workerPid });
|
|
415
|
+
const activeMutation = mutation && api.isAlive({ pid: mutation.pid, group: mutation.group });
|
|
416
|
+
if (!activeWorker && !activeMutation)
|
|
417
|
+
lastAttempt = {
|
|
418
|
+
...lastAttempt,
|
|
419
|
+
interrupted: true,
|
|
420
|
+
message: 'The updater was interrupted. The next interactive invocation retries this request safely.',
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const newer = !!offeredVersion && !!installedVersion && isOlderVersion(installedVersion, offeredVersion);
|
|
424
|
+
let state = offeredVersion
|
|
425
|
+
? newer
|
|
426
|
+
? 'update-available'
|
|
427
|
+
: 'up-to-date'
|
|
428
|
+
: 'unknown';
|
|
429
|
+
if (install.kind === 'bundled' || install.kind === 'image')
|
|
430
|
+
state = 'managed';
|
|
431
|
+
else if ((lastAttempt?.status === 'queued' || lastAttempt?.status === 'running') &&
|
|
432
|
+
installedVersion !== lastAttempt.version)
|
|
433
|
+
state = 'pending';
|
|
434
|
+
else if (lastAttempt?.status === 'failed' && newer)
|
|
435
|
+
state = 'failed';
|
|
436
|
+
else if (installedVersion && isOlderVersion(opts.currentVersion, installedVersion))
|
|
437
|
+
state = 'restart-pending';
|
|
438
|
+
let directory;
|
|
440
439
|
try {
|
|
441
|
-
|
|
442
|
-
const res = await fetch(url, {
|
|
443
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
444
|
-
headers: { accept: 'application/json' },
|
|
445
|
-
});
|
|
446
|
-
if (!res.ok)
|
|
447
|
-
return;
|
|
448
|
-
const body = (await res.json());
|
|
449
|
-
// Classify the install shape while we're already off the hot path — the
|
|
450
|
-
// `npm root -g` probe is the only piece that can cost real time and it is
|
|
451
|
-
// bounded by NPM_ROOT_TIMEOUT_MS. It only runs when the entry point even
|
|
452
|
-
// looks like an npm package dir.
|
|
453
|
-
let install;
|
|
454
|
-
if (resolveCliPackageDir()) {
|
|
455
|
-
install = classifyCliInstall(process.env, process.argv, await resolveNpmGlobalRoot());
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
install = classifyCliInstall(process.env, process.argv, null);
|
|
459
|
-
}
|
|
460
|
-
// Preserve a pending auto-update marker across refreshes so the
|
|
461
|
-
// once-per-window gate survives the cache being rewritten.
|
|
462
|
-
const prior = readCache();
|
|
463
|
-
writeCache({
|
|
464
|
-
lastCheckAt: Date.now(),
|
|
465
|
-
latest: typeof body.latest === 'string' ? body.latest : null,
|
|
466
|
-
npmPackage: typeof body.npmPackage === 'string' ? body.npmPackage : null,
|
|
467
|
-
baseURL: normalizeBaseUrl(baseURL),
|
|
468
|
-
install,
|
|
469
|
-
autoUpdate: prior?.autoUpdate,
|
|
470
|
-
});
|
|
440
|
+
directory = runtime().runtimeDir(packageRoot(argv));
|
|
471
441
|
}
|
|
472
442
|
catch {
|
|
473
|
-
|
|
443
|
+
directory = path.join(os.homedir(), '.skrr', 'cli-update-runtime');
|
|
474
444
|
}
|
|
445
|
+
let autoUpdateReason = null;
|
|
446
|
+
if (install.kind !== 'npm-global')
|
|
447
|
+
autoUpdateReason = `The ${install.kind} installation is owned by its installer.`;
|
|
448
|
+
else if (!install.writable)
|
|
449
|
+
autoUpdateReason = 'The npm global prefix is read-only.';
|
|
450
|
+
else if (process.platform === 'win32')
|
|
451
|
+
autoUpdateReason =
|
|
452
|
+
'Use npm directly on Windows; managed replacement requires POSIX process-group coordination.';
|
|
453
|
+
else if (!isPrivateCliPrefix(packageRoot(argv)))
|
|
454
|
+
autoUpdateReason = 'Shared or external npm prefixes require the owning package manager.';
|
|
455
|
+
else if (env.SKRR_CLI_UPDATE_SESSION_UNAVAILABLE === '1')
|
|
456
|
+
autoUpdateReason = 'A safe CLI session lease could not be recorded.';
|
|
457
|
+
else if (isAutoUpdateSuppressed(env))
|
|
458
|
+
autoUpdateReason = 'Automatic CLI updates are disabled by environment policy.';
|
|
459
|
+
return {
|
|
460
|
+
component: 'cli',
|
|
461
|
+
runningVersion: opts.currentVersion,
|
|
462
|
+
installedVersion,
|
|
463
|
+
offeredVersion,
|
|
464
|
+
owner: install.kind,
|
|
465
|
+
state,
|
|
466
|
+
autoUpdateAllowed: autoUpdateReason === null,
|
|
467
|
+
autoUpdateReason,
|
|
468
|
+
command: cliUpdateCommand(install.kind),
|
|
469
|
+
lastCheckAt: matchingCache?.lastCheckAt ?? null,
|
|
470
|
+
lastAttempt,
|
|
471
|
+
logPath: path.join(directory, 'update.log'),
|
|
472
|
+
message: lastAttempt?.message,
|
|
473
|
+
};
|
|
475
474
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
* `spawn` inside an 'exit' handler works because the fork itself is
|
|
487
|
-
* synchronous; only its events would need the (already drained) loop.
|
|
488
|
-
*
|
|
489
|
-
* The version comes from our own release feed's `latest`, validated to a
|
|
490
|
-
* semver-ish token so the win32 shell path can't smuggle syntax.
|
|
491
|
-
*/
|
|
492
|
-
function schedulePostExitSelfUpdate(pkg, version) {
|
|
493
|
-
if (autoUpdateScheduled)
|
|
494
|
-
return;
|
|
495
|
-
// Strict npm-name + semver-ish token validation — the win32 path spawns
|
|
496
|
-
// through a shell, so nothing with shell syntax may reach the spec.
|
|
497
|
-
const validPkg = /^@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/i.test(pkg) ||
|
|
498
|
-
/^[a-z0-9][a-z0-9._-]*$/i.test(pkg);
|
|
499
|
-
if (!validPkg || !/^[A-Za-z0-9._-]+$/.test(version))
|
|
500
|
-
return;
|
|
501
|
-
autoUpdateScheduled = true;
|
|
502
|
-
process.once('exit', () => {
|
|
475
|
+
/** Bounded discovery, also usable without a daemon, account, or TTY. */
|
|
476
|
+
async function checkCliUpdate(opts) {
|
|
477
|
+
const env = opts.env ?? process.env;
|
|
478
|
+
const argv = opts.argv ?? process.argv;
|
|
479
|
+
const cache = readCache(env);
|
|
480
|
+
const stale = !cache ||
|
|
481
|
+
cache.baseURL !== normalizeBaseUrl(opts.baseURL) ||
|
|
482
|
+
Date.now() - cache.lastCheckAt >= CHECK_TTL_MS;
|
|
483
|
+
let checkFailed = false;
|
|
484
|
+
if ((opts.force || stale) && (opts.force || !isTruthyEnv(env.OVERSKY_SKIP_UPDATE_CHECK))) {
|
|
503
485
|
try {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
486
|
+
// Independent probes share the same 1.5-second bound rather than adding
|
|
487
|
+
// an npm probe after the network timeout on every first invocation.
|
|
488
|
+
const [response, npmRoot] = await Promise.all([
|
|
489
|
+
fetch(`${opts.baseURL.replace(/\/+$/, '')}/api/config/cli-version`, {
|
|
490
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
491
|
+
headers: { accept: 'application/json' },
|
|
492
|
+
}),
|
|
493
|
+
resolveCliPackageDir(argv) ? resolveNpmGlobalRoot(env) : Promise.resolve(null),
|
|
494
|
+
]);
|
|
495
|
+
if (!response.ok)
|
|
496
|
+
throw new Error(`Version service returned HTTP ${response.status}`);
|
|
497
|
+
const body = (await response.json());
|
|
498
|
+
if (typeof body.latest !== 'string' ||
|
|
499
|
+
(0, auth_core_1.compareReleaseVersions)(body.latest, body.latest) === null)
|
|
500
|
+
throw new Error('Version service returned an invalid version');
|
|
501
|
+
writeCache({
|
|
502
|
+
lastCheckAt: Date.now(),
|
|
503
|
+
latest: body.latest,
|
|
504
|
+
npmPackage: CLI_PACKAGE,
|
|
505
|
+
baseURL: normalizeBaseUrl(opts.baseURL),
|
|
506
|
+
install: classifyCliInstall(env, argv, npmRoot),
|
|
507
|
+
packageRoot: packageRoot(argv),
|
|
508
|
+
npmGlobalRoot: npmRoot,
|
|
509
|
+
autoUpdate: cache?.autoUpdate,
|
|
510
|
+
}, env);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
checkFailed = true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const status = readCliUpdateStatus(opts);
|
|
517
|
+
if (checkFailed)
|
|
518
|
+
return {
|
|
519
|
+
...status,
|
|
520
|
+
state: 'check-failed',
|
|
521
|
+
message: 'Could not check for a newer CLI version. Any offered version is from the last successful check; retry `skrr update --check`.',
|
|
522
|
+
};
|
|
523
|
+
return status;
|
|
524
|
+
}
|
|
525
|
+
function npmExecutable(env) {
|
|
526
|
+
const name = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
527
|
+
for (const directory of (env.PATH ?? '').split(path.delimiter)) {
|
|
528
|
+
if (!directory || !path.isAbsolute(directory))
|
|
529
|
+
continue;
|
|
530
|
+
try {
|
|
531
|
+
const candidate = path.join(directory, name);
|
|
532
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
533
|
+
return fs.realpathSync(candidate);
|
|
511
534
|
}
|
|
512
535
|
catch {
|
|
513
|
-
/*
|
|
536
|
+
/* next PATH entry */
|
|
514
537
|
}
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
function updaterEnvironment(env) {
|
|
542
|
+
// The public npm package update does not require application credentials.
|
|
543
|
+
return Object.fromEntries(Object.entries(env).filter(([key]) => !/(?:TOKEN|SECRET|PASSWORD|CREDENTIAL|API_KEY|PRIVATE_KEY|AUTH_FD|AUTH_SOCK)/i.test(key) &&
|
|
544
|
+
!/^(?:SKRR_|OVERSKY_|OPENAI_|ANTHROPIC_|CODEX_|CLAUDE_|NODE_OPTIONS$)/i.test(key)));
|
|
545
|
+
}
|
|
546
|
+
function startWorker(directory, env) {
|
|
547
|
+
const sourceDir = path.resolve(__dirname, '..', '..', 'bin');
|
|
548
|
+
const workerBytes = fs.readFileSync(path.join(sourceDir, 'update-worker.cjs'));
|
|
549
|
+
const helperBytes = fs.readFileSync(path.join(sourceDir, 'update-session.cjs'));
|
|
550
|
+
const runnerBytes = fs.readFileSync(path.join(sourceDir, 'update-npm-runner.cjs'));
|
|
551
|
+
const digest = (0, node_crypto_1.createHash)('sha256')
|
|
552
|
+
.update(workerBytes)
|
|
553
|
+
.update(helperBytes)
|
|
554
|
+
.update(runnerBytes)
|
|
555
|
+
.digest('hex')
|
|
556
|
+
.slice(0, 24);
|
|
557
|
+
const immutableDir = path.join(directory, 'workers', digest);
|
|
558
|
+
fs.mkdirSync(immutableDir, { recursive: true, mode: 0o700 });
|
|
559
|
+
for (const [file, bytes] of [
|
|
560
|
+
['update-worker.cjs', workerBytes],
|
|
561
|
+
['update-session.cjs', helperBytes],
|
|
562
|
+
['update-npm-runner.cjs', runnerBytes],
|
|
563
|
+
]) {
|
|
564
|
+
const destination = path.join(immutableDir, file);
|
|
565
|
+
if (!fs.existsSync(destination))
|
|
566
|
+
fs.writeFileSync(destination, bytes, { mode: 0o600, flag: 'wx' });
|
|
567
|
+
}
|
|
568
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [path.join(immutableDir, 'update-worker.cjs'), directory], {
|
|
569
|
+
detached: true,
|
|
570
|
+
stdio: 'ignore',
|
|
571
|
+
env: updaterEnvironment(env),
|
|
572
|
+
});
|
|
573
|
+
child.once('error', () => {
|
|
574
|
+
/* persisted queue remains retryable */
|
|
515
575
|
});
|
|
576
|
+
child.unref();
|
|
516
577
|
}
|
|
517
|
-
/**
|
|
518
|
-
*
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
578
|
+
/** Queue an exact version through the owning global npm prefix. The current
|
|
579
|
+
* invocation stays leased, so replacement starts after it and peers exit. */
|
|
580
|
+
async function performCliUpdate(opts) {
|
|
581
|
+
const env = opts.env ?? process.env;
|
|
582
|
+
const argv = opts.argv ?? process.argv;
|
|
583
|
+
const status = await checkCliUpdate({ ...opts, force: opts.force ?? true });
|
|
584
|
+
if (status.state === 'check-failed')
|
|
585
|
+
return status;
|
|
586
|
+
const checkedCache = readCache(env);
|
|
587
|
+
const install = classifyCliInstall(env, argv, checkedCache?.packageRoot === packageRoot(argv) ? (checkedCache.npmGlobalRoot ?? null) : null);
|
|
588
|
+
if (install.kind !== 'npm-global' ||
|
|
589
|
+
install.writable !== true ||
|
|
590
|
+
!isPrivateCliPrefix(packageRoot(argv)) ||
|
|
591
|
+
env.SKRR_CLI_UPDATE_SESSION_UNAVAILABLE === '1') {
|
|
592
|
+
return {
|
|
593
|
+
...status,
|
|
594
|
+
message: `This CLI is owned by ${install.kind}${install.kind === 'npm-global' ? ' in a shared or external prefix' : ''}. ${cliUpdateCommand(install.kind)}.`,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const recoverableAttempt = status.lastAttempt && ['queued', 'running'].includes(status.lastAttempt.status);
|
|
598
|
+
if (!status.offeredVersion ||
|
|
599
|
+
!status.installedVersion ||
|
|
600
|
+
(!isOlderVersion(status.installedVersion, status.offeredVersion) && !recoverableAttempt))
|
|
601
|
+
return status;
|
|
602
|
+
const npmPath = npmExecutable(env);
|
|
603
|
+
if (!npmPath)
|
|
604
|
+
return {
|
|
605
|
+
...status,
|
|
606
|
+
state: 'failed',
|
|
607
|
+
message: 'npm is unavailable; use the package manager that installed this CLI.',
|
|
608
|
+
};
|
|
609
|
+
const api = runtime();
|
|
610
|
+
if (env.SKRR_CLI_UPDATE_SESSION_REGISTERED !== String(process.pid))
|
|
611
|
+
api.registerSession(packageRoot(argv));
|
|
612
|
+
const directory = api.runtimeDir(packageRoot(argv));
|
|
613
|
+
const release = api.acquireLock(path.join(directory, 'request.lock'), 1000);
|
|
525
614
|
try {
|
|
526
|
-
|
|
615
|
+
const old = attemptFor(argv);
|
|
616
|
+
if (!old || !['queued', 'running'].includes(old.status)) {
|
|
617
|
+
api.writeJson(path.join(directory, 'result.json'), {
|
|
618
|
+
status: 'queued',
|
|
619
|
+
version: status.offeredVersion,
|
|
620
|
+
attemptedAt: Date.now(),
|
|
621
|
+
packageRoot: packageRoot(argv),
|
|
622
|
+
npmPath,
|
|
623
|
+
coordinationVersion: api.COORDINATION_VERSION,
|
|
624
|
+
message: 'Waiting for active CLI sessions to exit; the next invocation uses the new version.',
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
startWorker(directory, env);
|
|
628
|
+
}
|
|
629
|
+
finally {
|
|
630
|
+
release();
|
|
631
|
+
}
|
|
632
|
+
return readCliUpdateStatus(opts);
|
|
633
|
+
}
|
|
634
|
+
/** One startup advisory across oclif command families. Awaiting bounded
|
|
635
|
+
* discovery makes the first invocation useful rather than losing its fetch
|
|
636
|
+
* when a short command exits. No update/check errors can break a command. */
|
|
637
|
+
async function maybeWarnOutdatedCli(opts) {
|
|
638
|
+
try {
|
|
639
|
+
const env = opts.env ?? process.env;
|
|
640
|
+
if (!process.stderr.isTTY || isNudgeSuppressed(env))
|
|
527
641
|
return;
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (!process.stderr.isTTY)
|
|
642
|
+
const status = await checkCliUpdate(opts);
|
|
643
|
+
if (status.state === 'check-failed')
|
|
531
644
|
return;
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
645
|
+
const cache = readCache(env);
|
|
646
|
+
if (!cache || cache.baseURL !== normalizeBaseUrl(opts.baseURL))
|
|
647
|
+
return;
|
|
648
|
+
const action = decideCliUpdateAction({
|
|
649
|
+
currentVersion: opts.currentVersion,
|
|
650
|
+
cache: {
|
|
651
|
+
...cache,
|
|
652
|
+
install: { kind: status.owner, writable: status.autoUpdateAllowed },
|
|
653
|
+
autoUpdate: status.lastAttempt
|
|
654
|
+
? {
|
|
655
|
+
attemptedAt: status.lastAttempt.attemptedAt,
|
|
656
|
+
version: status.lastAttempt.version,
|
|
657
|
+
status: status.lastAttempt.status,
|
|
658
|
+
}
|
|
659
|
+
: cache.autoUpdate,
|
|
660
|
+
},
|
|
661
|
+
env,
|
|
662
|
+
isTTY: true,
|
|
663
|
+
});
|
|
664
|
+
if (action?.type === 'auto-update') {
|
|
665
|
+
const queued = await performCliUpdate({ ...opts, force: false });
|
|
666
|
+
if (queued.state === 'pending')
|
|
667
|
+
process.stderr.write(`[skrr] CLI update to ${queued.lastAttempt?.version ?? action.version} queued in the background until active CLI sessions exit. Disable with OVERSKY_CLI_AUTO_UPDATE=0.\n`);
|
|
668
|
+
else if (queued.message)
|
|
669
|
+
process.stderr.write(`[skrr] ${queued.message}\n`);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
// Resume a queue whose worker exhausted its bounded wait, without
|
|
673
|
+
// classifying queued/running work as a failed npm install.
|
|
674
|
+
if (status.lastAttempt &&
|
|
675
|
+
['queued', 'running'].includes(status.lastAttempt.status) &&
|
|
676
|
+
status.autoUpdateAllowed)
|
|
677
|
+
await performCliUpdate({ ...opts, force: false });
|
|
551
678
|
if (action)
|
|
552
679
|
process.stderr.write(action.line);
|
|
553
680
|
}
|
|
554
|
-
const stale = !cache || cache.baseURL !== normalized || Date.now() - cache.lastCheckAt > CHECK_TTL_MS;
|
|
555
|
-
if (stale) {
|
|
556
|
-
// Fire-and-forget. Not awaited, so it adds no latency; if the command
|
|
557
|
-
// exits before it resolves the refresh is simply dropped and retried
|
|
558
|
-
// on the next invocation.
|
|
559
|
-
void refreshCache(opts.baseURL);
|
|
560
|
-
}
|
|
561
681
|
}
|
|
562
682
|
catch {
|
|
563
|
-
|
|
683
|
+
/* version discovery must never prevent the user's command */
|
|
564
684
|
}
|
|
565
685
|
}
|
|
686
|
+
function shouldCheckCliUpdateAtStartup(argv, isTTY, env = process.env) {
|
|
687
|
+
if (!isTTY || isNudgeSuppressed(env) || isTruthyEnv(env.CI))
|
|
688
|
+
return false;
|
|
689
|
+
if (argv.some((arg) => /^(?:--json(?:=|$)|--help$|-h$|--version$|-v$|--headless$|--print$|-p$|--non-interactive$|--no-interactive$)/.test(arg)))
|
|
690
|
+
return false;
|
|
691
|
+
const command = argv
|
|
692
|
+
.filter((arg) => !arg.startsWith('-'))
|
|
693
|
+
.slice(0, 2)
|
|
694
|
+
.join(':');
|
|
695
|
+
return ![
|
|
696
|
+
'update',
|
|
697
|
+
'code:run',
|
|
698
|
+
'code:acp',
|
|
699
|
+
'code:update',
|
|
700
|
+
'code:install',
|
|
701
|
+
'daemon:update',
|
|
702
|
+
'daemons:update',
|
|
703
|
+
].includes(command);
|
|
704
|
+
}
|