@skrr-ai/cli 0.1.45 → 0.1.47
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/dist/base-command.d.ts +13 -1
- package/dist/base-command.js +37 -3
- package/dist/commands/agents/actions/create.js +11 -4
- package/dist/commands/browser/cloud-agents.js +4 -0
- package/dist/commands/code/handover.d.ts +11 -0
- package/dist/commands/code/handover.js +104 -1
- package/dist/commands/code/install.js +31 -0
- package/dist/commands/code/update.d.ts +46 -0
- package/dist/commands/code/update.js +242 -0
- package/dist/commands/commitments/handover.d.ts +29 -0
- package/dist/commands/commitments/handover.js +99 -0
- package/dist/commands/convos/handover.d.ts +1 -0
- package/dist/commands/convos/handover.js +4 -0
- package/dist/commands/daemon/index.js +2 -0
- package/dist/commands/daemon/update.d.ts +19 -0
- package/dist/commands/daemon/update.js +48 -0
- package/dist/commands/followups/watch.js +40 -5
- package/dist/commands/spaces/briefs/get.js +8 -0
- package/dist/commands/spaces/briefs/list.js +2 -0
- package/dist/commands/spaces/briefs/set.d.ts +3 -0
- package/dist/commands/spaces/briefs/set.js +78 -13
- package/dist/commands/spaces/expertise-suggestions.d.ts +12 -0
- package/dist/commands/spaces/expertise-suggestions.js +71 -0
- package/dist/commands/spaces/members/add.d.ts +1 -0
- package/dist/commands/spaces/members/add.js +11 -3
- package/dist/commands/spaces/members/list.js +11 -0
- package/dist/commands/tasks/assign.d.ts +9 -0
- package/dist/commands/tasks/assign.js +108 -1
- package/dist/commands/tasks/fork.d.ts +32 -0
- package/dist/commands/tasks/fork.js +171 -0
- package/dist/commands/tasks/handover.d.ts +23 -0
- package/dist/commands/tasks/handover.js +205 -0
- package/dist/commands/tasks/promote.d.ts +23 -0
- package/dist/commands/tasks/promote.js +82 -0
- package/dist/commands/tasks/runs.d.ts +16 -0
- package/dist/commands/tasks/runs.js +46 -1
- package/dist/commands/tasks/show.d.ts +23 -0
- package/dist/commands/tasks/show.js +59 -0
- package/dist/lib/api-fetch.d.ts +7 -1
- package/dist/lib/api-fetch.js +9 -2
- package/dist/lib/assignee-resolver.d.ts +22 -0
- package/dist/lib/assignee-resolver.js +26 -0
- package/dist/lib/code-handover.d.ts +50 -0
- package/dist/lib/code-handover.js +135 -0
- package/dist/lib/commitments.d.ts +22 -0
- package/dist/lib/commitments.js +42 -0
- package/dist/lib/dedicated-wait.js +22 -1
- package/dist/lib/followups.d.ts +18 -0
- package/dist/lib/followups.js +44 -0
- package/dist/lib/node-adapter.js +6 -0
- package/dist/lib/task-extras.d.ts +44 -0
- package/dist/lib/task-extras.js +73 -0
- package/dist/lib/update-check.d.ts +80 -7
- package/dist/lib/update-check.js +365 -33
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +16 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +10 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +16 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +5 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +2262 -2241
- package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
- package/oclif.manifest.json +34562 -33947
- package/package.json +2 -2
|
@@ -1,11 +1,37 @@
|
|
|
1
|
+
export type CliInstallKind = 'npm-global' | 'npm-local' | 'dev' | 'bundled' | 'other';
|
|
2
|
+
interface CheckCache {
|
|
3
|
+
/** Epoch ms of the last successful refresh. */
|
|
4
|
+
lastCheckAt: number;
|
|
5
|
+
/** Latest advertised CLI version, or null if the API had none. */
|
|
6
|
+
latest: string | null;
|
|
7
|
+
/** npm package to update (advisory copy), or null. */
|
|
8
|
+
npmPackage: string | null;
|
|
9
|
+
/** baseURL the cache was fetched against — invalidates on server switch. */
|
|
10
|
+
baseURL: string;
|
|
11
|
+
/** Resolved install shape, classified during the last refresh. */
|
|
12
|
+
install?: {
|
|
13
|
+
kind: CliInstallKind;
|
|
14
|
+
writable?: boolean;
|
|
15
|
+
};
|
|
16
|
+
/** Recorded BEFORE a background self-update is scheduled. */
|
|
17
|
+
autoUpdate?: {
|
|
18
|
+
attemptedAt: number;
|
|
19
|
+
version: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
1
22
|
/** Trailing-slash- and case-insensitive normalization for baseURL compares. */
|
|
2
23
|
export declare function normalizeBaseUrl(url: string): string;
|
|
3
24
|
/**
|
|
4
25
|
* True when the advisory must stay silent regardless of version state:
|
|
5
|
-
* explicit suppression,
|
|
6
|
-
*
|
|
26
|
+
* explicit suppression, a desktop-app-bundled CLI (which auto-updates with
|
|
27
|
+
* the app and must not be told to `npm update -g`), or an image-baked install
|
|
28
|
+
* the control plane owns.
|
|
7
29
|
*/
|
|
8
30
|
export declare function isNudgeSuppressed(env?: NodeJS.ProcessEnv): boolean;
|
|
31
|
+
/** True when the background self-update is allowed to run at all. Suppression
|
|
32
|
+
* implies no auto-update — a user who asked for silence did not ask for a
|
|
33
|
+
* silent mutation of their global npm. */
|
|
34
|
+
export declare function isAutoUpdateSuppressed(env?: NodeJS.ProcessEnv): boolean;
|
|
9
35
|
/**
|
|
10
36
|
* True iff `current` is strictly older than `latest` by numeric release
|
|
11
37
|
* core. Conservative: any unparseable input returns false (no nudge).
|
|
@@ -13,19 +39,66 @@ export declare function isNudgeSuppressed(env?: NodeJS.ProcessEnv): boolean;
|
|
|
13
39
|
* we never nag someone testing a prerelease of the latest line.
|
|
14
40
|
*/
|
|
15
41
|
export declare function isOlderVersion(current: string, latest: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the package directory this process was launched from when it lives
|
|
44
|
+
* under `node_modules/@skrr-ai/cli` (any npm install — global or project-local),
|
|
45
|
+
* or null. Probes both the raw entry path and its realpath because the bin is
|
|
46
|
+
* usually a symlink into the package.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveCliPackageDir(argv?: readonly string[]): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Classify how this CLI was installed. `npmGlobalRoot` (from `npm root -g`)
|
|
51
|
+
* distinguishes a global install — which we may auto-update — from a
|
|
52
|
+
* project-local dependency, whose bytes belong to that project's package.json.
|
|
53
|
+
* `dev` is a checkout launched via its own `bin/run.js`: a `src/commands`
|
|
54
|
+
* dir beside `dist` exists only in a checkout — published packages ship
|
|
55
|
+
* `dist` and no `src`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function classifyCliInstall(env: NodeJS.ProcessEnv, argv: readonly string[], npmGlobalRoot: string | null): {
|
|
58
|
+
kind: CliInstallKind;
|
|
59
|
+
writable?: boolean;
|
|
60
|
+
};
|
|
16
61
|
/**
|
|
17
62
|
* Build the one-line stderr advisory, or null when no nudge is warranted.
|
|
18
|
-
*
|
|
63
|
+
* `installKind` tailors the command to the bytes' owner: a project-local dep
|
|
64
|
+
* is updated inside its project, a dev checkout is rebuilt — telling either
|
|
65
|
+
* to `npm update -g` would fix the wrong install.
|
|
19
66
|
*/
|
|
20
|
-
export declare function buildNudgeLine(currentVersion: string, latest: string | null, npmPackage: string | null): string | null;
|
|
67
|
+
export declare function buildNudgeLine(currentVersion: string, latest: string | null, npmPackage: string | null, installKind?: CliInstallKind): string | null;
|
|
68
|
+
export type CliUpdateAction = {
|
|
69
|
+
type: 'nudge';
|
|
70
|
+
line: string;
|
|
71
|
+
} | {
|
|
72
|
+
type: 'auto-update';
|
|
73
|
+
version: string;
|
|
74
|
+
pkg: string;
|
|
75
|
+
line: string;
|
|
76
|
+
} | null;
|
|
77
|
+
/**
|
|
78
|
+
* Decide, synchronously and from cache alone, what this command should do
|
|
79
|
+
* about its version state. Pure — exported for direct unit testing.
|
|
80
|
+
*
|
|
81
|
+
* AUTO-UPDATE fires at most once per TTL window per target version, and only
|
|
82
|
+
* for a provably writable npm-global install on an interactive TTY. An attempt
|
|
83
|
+
* that didn't take (still outdated next run) falls back to the manual nudge
|
|
84
|
+
* rather than retrying immediately.
|
|
85
|
+
*/
|
|
86
|
+
export declare function decideCliUpdateAction(args: {
|
|
87
|
+
currentVersion: string;
|
|
88
|
+
cache: CheckCache | null;
|
|
89
|
+
env: NodeJS.ProcessEnv;
|
|
90
|
+
isTTY: boolean;
|
|
91
|
+
now?: number;
|
|
92
|
+
}): CliUpdateAction;
|
|
21
93
|
/**
|
|
22
94
|
* Entry point, called once per command from `BaseCommand.init()`.
|
|
23
95
|
*
|
|
24
|
-
* Synchronous by design: it decides
|
|
25
|
-
*
|
|
26
|
-
*
|
|
96
|
+
* Synchronous by design: it decides from the cache (no network on the hot
|
|
97
|
+
* path) and only kicks off a background refresh when the cache is stale,
|
|
98
|
+
* missing, or from a different server. Returns immediately.
|
|
27
99
|
*/
|
|
28
100
|
export declare function maybeWarnOutdatedCli(opts: {
|
|
29
101
|
currentVersion: string;
|
|
30
102
|
baseURL: string;
|
|
31
103
|
}): void;
|
|
104
|
+
export {};
|
package/dist/lib/update-check.js
CHANGED
|
@@ -35,39 +35,73 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.normalizeBaseUrl = normalizeBaseUrl;
|
|
37
37
|
exports.isNudgeSuppressed = isNudgeSuppressed;
|
|
38
|
+
exports.isAutoUpdateSuppressed = isAutoUpdateSuppressed;
|
|
38
39
|
exports.isOlderVersion = isOlderVersion;
|
|
40
|
+
exports.resolveCliPackageDir = resolveCliPackageDir;
|
|
41
|
+
exports.classifyCliInstall = classifyCliInstall;
|
|
39
42
|
exports.buildNudgeLine = buildNudgeLine;
|
|
43
|
+
exports.decideCliUpdateAction = decideCliUpdateAction;
|
|
40
44
|
exports.maybeWarnOutdatedCli = maybeWarnOutdatedCli;
|
|
45
|
+
const node_child_process_1 = require("node:child_process");
|
|
41
46
|
const fs = __importStar(require("node:fs"));
|
|
42
47
|
const os = __importStar(require("node:os"));
|
|
43
48
|
const path = __importStar(require("node:path"));
|
|
49
|
+
const node_util_1 = require("node:util");
|
|
44
50
|
/**
|
|
45
|
-
* update-check.ts — non-blocking "a newer `skrr` is available" advisory
|
|
51
|
+
* update-check.ts — non-blocking "a newer `skrr` is available" advisory AND
|
|
52
|
+
* background self-update for installs we own.
|
|
46
53
|
*
|
|
47
|
-
* The standalone `skrr` CLI (npm `@skrr-ai/cli`) has no
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* ~4h on-disk cache, with ZERO added latency — whether to print a single
|
|
52
|
-
* line to stderr, and refreshes that cache in the background.
|
|
54
|
+
* The standalone `skrr` CLI (npm `@skrr-ai/cli`) has no in-place update path
|
|
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:
|
|
53
58
|
*
|
|
54
|
-
*
|
|
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):
|
|
55
73
|
* - Writes ONLY to stderr, never stdout, so `--json` output stays clean.
|
|
56
|
-
* - Prints at most ONE line, and only on an interactive TTY
|
|
57
|
-
*
|
|
58
|
-
* - Never blocks the command: the
|
|
59
|
-
*
|
|
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.
|
|
60
80
|
* - Never throws: every path is wrapped so a malformed cache, offline
|
|
61
|
-
* API, or read-only home directory can't break a command.
|
|
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.
|
|
62
84
|
* - Suppressible: `OVERSKY_NO_UPDATE_NUDGE` / `OVERSKY_SKIP_UPDATE_CHECK`
|
|
63
|
-
* silence
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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.
|
|
66
88
|
*/
|
|
67
89
|
/** How long a cached advisory stays fresh before a background refresh. */
|
|
68
90
|
const CHECK_TTL_MS = 4 * 60 * 60 * 1000; // 4h
|
|
69
91
|
/** Hard ceiling on the background refresh request. */
|
|
70
92
|
const FETCH_TIMEOUT_MS = 1500;
|
|
93
|
+
/** Hard ceiling on the `npm root -g` classification probe. */
|
|
94
|
+
const NPM_ROOT_TIMEOUT_MS = 1500;
|
|
95
|
+
/** Marker written by the dedicated-runtime image bake — a guest's skrr is
|
|
96
|
+
* image-pinned and must never be told to `npm update -g`. */
|
|
97
|
+
const DEDICATED_RUNTIME_ENV_FILE = '/etc/skrr-dedicated-runtime.env';
|
|
98
|
+
/** Daemon-source values for image-pinned (hosted) runtime contexts. */
|
|
99
|
+
const HOSTED_RUNTIME_SOURCES = new Set([
|
|
100
|
+
'dedicated-runtime',
|
|
101
|
+
'hosted-machine',
|
|
102
|
+
'cloud-coding',
|
|
103
|
+
]);
|
|
104
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
71
105
|
function cacheFilePath() {
|
|
72
106
|
return path.join(os.homedir(), '.skrr', 'cli-update-check.json');
|
|
73
107
|
}
|
|
@@ -80,15 +114,41 @@ function isTruthyEnv(value) {
|
|
|
80
114
|
return false;
|
|
81
115
|
return ['1', 'true', 'on', 'yes'].includes(value.trim().toLowerCase());
|
|
82
116
|
}
|
|
117
|
+
function isDisabledEnv(value) {
|
|
118
|
+
if (!value)
|
|
119
|
+
return false;
|
|
120
|
+
return ['0', 'false', 'off', 'no', 'disabled'].includes(value.trim().toLowerCase());
|
|
121
|
+
}
|
|
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
|
+
function isHostedRuntimeGuest(env) {
|
|
126
|
+
if (HOSTED_RUNTIME_SOURCES.has((env.OVERSKY_DAEMON_SOURCE ?? '').trim()))
|
|
127
|
+
return true;
|
|
128
|
+
try {
|
|
129
|
+
return fs.existsSync(DEDICATED_RUNTIME_ENV_FILE);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
83
135
|
/**
|
|
84
136
|
* True when the advisory must stay silent regardless of version state:
|
|
85
|
-
* explicit suppression,
|
|
86
|
-
*
|
|
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.
|
|
87
140
|
*/
|
|
88
141
|
function isNudgeSuppressed(env = process.env) {
|
|
89
142
|
return (isTruthyEnv(env.OVERSKY_NO_UPDATE_NUDGE) ||
|
|
90
143
|
isTruthyEnv(env.OVERSKY_SKIP_UPDATE_CHECK) ||
|
|
91
|
-
env.OVERSKY_CLI_BUNDLED === '1'
|
|
144
|
+
env.OVERSKY_CLI_BUNDLED === '1' ||
|
|
145
|
+
isHostedRuntimeGuest(env));
|
|
146
|
+
}
|
|
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
|
+
function isAutoUpdateSuppressed(env = process.env) {
|
|
151
|
+
return isNudgeSuppressed(env) || isDisabledEnv(env.OVERSKY_CLI_AUTO_UPDATE);
|
|
92
152
|
}
|
|
93
153
|
/**
|
|
94
154
|
* Parse the numeric release core of a version (`1.2.3-rc.1` → `[1,2,3]`),
|
|
@@ -130,18 +190,202 @@ function isOlderVersion(current, latest) {
|
|
|
130
190
|
}
|
|
131
191
|
return false;
|
|
132
192
|
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Install-shape classification
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
const CLI_PACKAGE_DIR_RE = /^(.*[/\\]node_modules[/\\]@skrr-ai[/\\]cli)[/\\]/;
|
|
197
|
+
/**
|
|
198
|
+
* Resolve the package directory this process was launched from when it lives
|
|
199
|
+
* under `node_modules/@skrr-ai/cli` (any npm install — global or project-local),
|
|
200
|
+
* or null. Probes both the raw entry path and its realpath because the bin is
|
|
201
|
+
* usually a symlink into the package.
|
|
202
|
+
*/
|
|
203
|
+
function resolveCliPackageDir(argv = process.argv) {
|
|
204
|
+
const entry = argv[1];
|
|
205
|
+
if (!entry)
|
|
206
|
+
return null;
|
|
207
|
+
const candidates = [entry];
|
|
208
|
+
try {
|
|
209
|
+
candidates.push(fs.realpathSync(entry));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
/* entry may be virtual; the raw form is still probed */
|
|
213
|
+
}
|
|
214
|
+
for (const candidate of candidates) {
|
|
215
|
+
const match = candidate.match(CLI_PACKAGE_DIR_RE);
|
|
216
|
+
if (match)
|
|
217
|
+
return match[1];
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
function dirWritable(dir) {
|
|
222
|
+
try {
|
|
223
|
+
const st = fs.statSync(dir);
|
|
224
|
+
if (!st.isDirectory())
|
|
225
|
+
return false;
|
|
226
|
+
const probe = path.join(dir, `.skrr.write-probe.${process.pid}.${Date.now()}`);
|
|
227
|
+
fs.closeSync(fs.openSync(probe, 'w', 0o600));
|
|
228
|
+
fs.unlinkSync(probe);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Classify how this CLI was installed. `npmGlobalRoot` (from `npm root -g`)
|
|
237
|
+
* distinguishes a global install — which we may auto-update — from a
|
|
238
|
+
* project-local dependency, whose bytes belong to that project's package.json.
|
|
239
|
+
* `dev` is a checkout launched via its own `bin/run.js`: a `src/commands`
|
|
240
|
+
* dir beside `dist` exists only in a checkout — published packages ship
|
|
241
|
+
* `dist` and no `src`.
|
|
242
|
+
*/
|
|
243
|
+
function classifyCliInstall(env, argv, npmGlobalRoot) {
|
|
244
|
+
if (env.OVERSKY_CLI_BUNDLED === '1' || isHostedRuntimeGuest(env)) {
|
|
245
|
+
return { kind: 'bundled' };
|
|
246
|
+
}
|
|
247
|
+
const packageDir = resolveCliPackageDir(argv);
|
|
248
|
+
if (!packageDir) {
|
|
249
|
+
// A dev checkout launches `cli/bin/run.js` directly — a `src/commands`
|
|
250
|
+
// sibling of `dist` exists only in a checkout (published packages ship
|
|
251
|
+
// `dist` and no `src`). Those bytes belong to git, not npm.
|
|
252
|
+
const entry = argv[1];
|
|
253
|
+
if (entry) {
|
|
254
|
+
try {
|
|
255
|
+
const cliRoot = path.resolve(path.dirname(entry), '..');
|
|
256
|
+
if (fs.existsSync(path.join(cliRoot, 'src', 'commands'))) {
|
|
257
|
+
return { kind: 'dev' };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
/* fall through to other */
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { kind: 'other' };
|
|
265
|
+
}
|
|
266
|
+
const nodeModulesDir = path.dirname(path.dirname(packageDir)); // …/node_modules
|
|
267
|
+
if (npmGlobalRoot) {
|
|
268
|
+
let resolvedGlobal = npmGlobalRoot;
|
|
269
|
+
try {
|
|
270
|
+
resolvedGlobal = fs.realpathSync(npmGlobalRoot);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
/* compare the raw form */
|
|
274
|
+
}
|
|
275
|
+
let resolvedNodeModules = nodeModulesDir;
|
|
276
|
+
try {
|
|
277
|
+
resolvedNodeModules = fs.realpathSync(nodeModulesDir);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
/* compare the raw form */
|
|
281
|
+
}
|
|
282
|
+
if (resolvedNodeModules !== resolvedGlobal)
|
|
283
|
+
return { kind: 'npm-local' };
|
|
284
|
+
}
|
|
285
|
+
else if (!/[/\\](lib|npm)[/\\]node_modules$/.test(nodeModulesDir)) {
|
|
286
|
+
// `npm root -g` gave no answer, so prove the global shape from the path
|
|
287
|
+
// instead: npm's unix global root is always `<prefix>/lib/node_modules`
|
|
288
|
+
// (incl. nvm/volta) and on Windows `%APPDATA%\npm\node_modules`. A
|
|
289
|
+
// project-local `<project>/node_modules` never matches either — without
|
|
290
|
+
// proof we must not treat it as ours to auto-update.
|
|
291
|
+
return { kind: 'npm-local' };
|
|
292
|
+
}
|
|
293
|
+
return { kind: 'npm-global', writable: dirWritable(nodeModulesDir) };
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Resolve `npm root -g` for install classification. Runs ONLY inside the
|
|
297
|
+
* background refresh — never on the command's hot path — and fails soft:
|
|
298
|
+
* no npm, a timeout, or an odd answer just leaves classification to the
|
|
299
|
+
* path shape alone.
|
|
300
|
+
*/
|
|
301
|
+
async function resolveNpmGlobalRoot() {
|
|
302
|
+
try {
|
|
303
|
+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
304
|
+
const { stdout } = await execFileAsync(npmBin, ['root', '-g'], {
|
|
305
|
+
timeout: NPM_ROOT_TIMEOUT_MS,
|
|
306
|
+
maxBuffer: 64 * 1024,
|
|
307
|
+
shell: process.platform === 'win32',
|
|
308
|
+
});
|
|
309
|
+
const root = stdout.trim();
|
|
310
|
+
return root && path.isAbsolute(root) ? root : null;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Advisory copy — one line, per install shape
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
133
319
|
/**
|
|
134
320
|
* Build the one-line stderr advisory, or null when no nudge is warranted.
|
|
135
|
-
*
|
|
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.
|
|
136
324
|
*/
|
|
137
|
-
function buildNudgeLine(currentVersion, latest, npmPackage) {
|
|
325
|
+
function buildNudgeLine(currentVersion, latest, npmPackage, installKind) {
|
|
138
326
|
if (!latest || !isOlderVersion(currentVersion, latest))
|
|
139
327
|
return null;
|
|
140
328
|
const pkg = npmPackage || '@skrr-ai/cli';
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
329
|
+
const prefix = `[skrr] Update available: ${currentVersion} → ${latest}. `;
|
|
330
|
+
const suffix = ` Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`;
|
|
331
|
+
switch (installKind) {
|
|
332
|
+
case 'npm-local':
|
|
333
|
+
return `${prefix}Run \`npm update ${pkg}\` in the owning project.${suffix}`;
|
|
334
|
+
case 'dev':
|
|
335
|
+
return `${prefix}Rebuild your checkout (\`npm --prefix cli run build\`).${suffix}`;
|
|
336
|
+
case 'other':
|
|
337
|
+
return `${prefix}Update via your install method — see https://skrr.ai.${suffix}`;
|
|
338
|
+
case 'npm-global':
|
|
339
|
+
case 'bundled':
|
|
340
|
+
default:
|
|
341
|
+
return `${prefix}Run \`npm update -g ${pkg}\` (or update the skrr app).${suffix}`;
|
|
342
|
+
}
|
|
144
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Decide, synchronously and from cache alone, what this command should do
|
|
346
|
+
* about its version state. Pure — exported for direct unit testing.
|
|
347
|
+
*
|
|
348
|
+
* AUTO-UPDATE fires at most once per TTL window per target version, and only
|
|
349
|
+
* for a provably writable npm-global install on an interactive TTY. An attempt
|
|
350
|
+
* that didn't take (still outdated next run) falls back to the manual nudge
|
|
351
|
+
* rather than retrying immediately.
|
|
352
|
+
*/
|
|
353
|
+
function decideCliUpdateAction(args) {
|
|
354
|
+
const { currentVersion, cache, env, isTTY } = args;
|
|
355
|
+
const now = args.now ?? Date.now();
|
|
356
|
+
if (!isTTY || isNudgeSuppressed(env))
|
|
357
|
+
return null;
|
|
358
|
+
if (!cache || !cache.latest || !isOlderVersion(currentVersion, cache.latest))
|
|
359
|
+
return null;
|
|
360
|
+
const install = cache.install ?? { kind: 'other' };
|
|
361
|
+
const pkg = cache.npmPackage || '@skrr-ai/cli';
|
|
362
|
+
const attempt = cache.autoUpdate;
|
|
363
|
+
const attemptFresh = attempt != null && attempt.version === cache.latest && now - attempt.attemptedAt < CHECK_TTL_MS;
|
|
364
|
+
if (install.kind === 'npm-global' && install.writable === true && !isAutoUpdateSuppressed(env)) {
|
|
365
|
+
if (!attemptFresh) {
|
|
366
|
+
return {
|
|
367
|
+
type: 'auto-update',
|
|
368
|
+
version: cache.latest,
|
|
369
|
+
pkg,
|
|
370
|
+
line: `[skrr] Updating ${currentVersion} → ${cache.latest} in the background ` +
|
|
371
|
+
`(\`npm install -g ${pkg}@${cache.latest}\`). Disable with OVERSKY_CLI_AUTO_UPDATE=0.\n`,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
// We already tried this version within the window and the update did not
|
|
375
|
+
// take — tell the user the manual command instead of retrying every run.
|
|
376
|
+
return {
|
|
377
|
+
type: 'nudge',
|
|
378
|
+
line: `[skrr] Update available: ${currentVersion} → ${cache.latest}. ` +
|
|
379
|
+
`The background update did not take — run \`npm update -g ${pkg}\` manually.` +
|
|
380
|
+
` Silence with OVERSKY_NO_UPDATE_NUDGE=1.\n`,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const line = buildNudgeLine(currentVersion, cache.latest, cache.npmPackage, install.kind);
|
|
384
|
+
return line ? { type: 'nudge', line } : null;
|
|
385
|
+
}
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// Cache I/O
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
145
389
|
function readCache() {
|
|
146
390
|
try {
|
|
147
391
|
const raw = fs.readFileSync(cacheFilePath(), 'utf8');
|
|
@@ -152,6 +396,19 @@ function readCache() {
|
|
|
152
396
|
latest: typeof parsed.latest === 'string' ? parsed.latest : null,
|
|
153
397
|
npmPackage: typeof parsed.npmPackage === 'string' ? parsed.npmPackage : null,
|
|
154
398
|
baseURL: parsed.baseURL,
|
|
399
|
+
install: parsed.install && typeof parsed.install.kind === 'string'
|
|
400
|
+
? {
|
|
401
|
+
kind: parsed.install.kind,
|
|
402
|
+
writable: typeof parsed.install.writable === 'boolean'
|
|
403
|
+
? parsed.install.writable
|
|
404
|
+
: undefined,
|
|
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,
|
|
155
412
|
};
|
|
156
413
|
}
|
|
157
414
|
}
|
|
@@ -172,6 +429,13 @@ function writeCache(cache) {
|
|
|
172
429
|
// Best-effort — a read-only home just means we re-check next run.
|
|
173
430
|
}
|
|
174
431
|
}
|
|
432
|
+
/** Persist the auto-update attempt marker without losing the rest of the cache. */
|
|
433
|
+
function markAutoUpdateAttempt(version) {
|
|
434
|
+
const cache = readCache();
|
|
435
|
+
if (!cache)
|
|
436
|
+
return;
|
|
437
|
+
writeCache({ ...cache, autoUpdate: { attemptedAt: Date.now(), version } });
|
|
438
|
+
}
|
|
175
439
|
async function refreshCache(baseURL) {
|
|
176
440
|
try {
|
|
177
441
|
const url = `${baseURL.replace(/\/+$/, '')}/api/config/cli-version`;
|
|
@@ -182,23 +446,80 @@ async function refreshCache(baseURL) {
|
|
|
182
446
|
if (!res.ok)
|
|
183
447
|
return;
|
|
184
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();
|
|
185
463
|
writeCache({
|
|
186
464
|
lastCheckAt: Date.now(),
|
|
187
465
|
latest: typeof body.latest === 'string' ? body.latest : null,
|
|
188
466
|
npmPackage: typeof body.npmPackage === 'string' ? body.npmPackage : null,
|
|
189
467
|
baseURL: normalizeBaseUrl(baseURL),
|
|
468
|
+
install,
|
|
469
|
+
autoUpdate: prior?.autoUpdate,
|
|
190
470
|
});
|
|
191
471
|
}
|
|
192
472
|
catch {
|
|
193
473
|
// Offline / timeout / bad payload — leave the old cache, retry later.
|
|
194
474
|
}
|
|
195
475
|
}
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
// Deferred self-update
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
let autoUpdateScheduled = false;
|
|
480
|
+
/**
|
|
481
|
+
* Schedule `npm install -g <pkg>@<version>` to run AFTER this process exits.
|
|
482
|
+
*
|
|
483
|
+
* Why deferred: oclif lazily loads command modules from disk, so swapping the
|
|
484
|
+
* package while a command runs could hand it a mid-replace `dist/`. The child
|
|
485
|
+
* is detached + stdio-ignored so it outlives this process and holds no pipes.
|
|
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', () => {
|
|
503
|
+
try {
|
|
504
|
+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
505
|
+
const child = (0, node_child_process_1.spawn)(npmBin, ['install', '-g', `${pkg}@${version}`], {
|
|
506
|
+
detached: true,
|
|
507
|
+
stdio: 'ignore',
|
|
508
|
+
shell: process.platform === 'win32',
|
|
509
|
+
});
|
|
510
|
+
child.unref();
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
/* a failed self-update just means the nudge returns next run */
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
196
517
|
/**
|
|
197
518
|
* Entry point, called once per command from `BaseCommand.init()`.
|
|
198
519
|
*
|
|
199
|
-
* Synchronous by design: it decides
|
|
200
|
-
*
|
|
201
|
-
*
|
|
520
|
+
* Synchronous by design: it decides from the cache (no network on the hot
|
|
521
|
+
* path) and only kicks off a background refresh when the cache is stale,
|
|
522
|
+
* missing, or from a different server. Returns immediately.
|
|
202
523
|
*/
|
|
203
524
|
function maybeWarnOutdatedCli(opts) {
|
|
204
525
|
try {
|
|
@@ -211,13 +532,24 @@ function maybeWarnOutdatedCli(opts) {
|
|
|
211
532
|
const normalized = normalizeBaseUrl(opts.baseURL);
|
|
212
533
|
const cache = readCache();
|
|
213
534
|
// Decide from whatever we last saw. A STALE cache is still a valid
|
|
214
|
-
// signal — CLI versions only move forward — so we
|
|
215
|
-
//
|
|
216
|
-
//
|
|
535
|
+
// signal — CLI versions only move forward — so we act on it and refresh
|
|
536
|
+
// in the background for next time. We only trust a cache that was fetched
|
|
537
|
+
// against the same server the CLI is currently pointed at.
|
|
217
538
|
if (cache && cache.baseURL === normalized) {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
539
|
+
const action = decideCliUpdateAction({
|
|
540
|
+
currentVersion: opts.currentVersion,
|
|
541
|
+
cache,
|
|
542
|
+
env: process.env,
|
|
543
|
+
isTTY: true,
|
|
544
|
+
});
|
|
545
|
+
if (action?.type === 'auto-update') {
|
|
546
|
+
// Record BEFORE scheduling so a same-second second invocation sees the
|
|
547
|
+
// attempt and doesn't double-spawn.
|
|
548
|
+
markAutoUpdateAttempt(action.version);
|
|
549
|
+
schedulePostExitSelfUpdate(action.pkg, action.version);
|
|
550
|
+
}
|
|
551
|
+
if (action)
|
|
552
|
+
process.stderr.write(action.line);
|
|
221
553
|
}
|
|
222
554
|
const stale = !cache || cache.baseURL !== normalized || Date.now() - cache.lastCheckAt > CHECK_TTL_MS;
|
|
223
555
|
if (stale) {
|
|
@@ -313,6 +313,21 @@ declare const ENGINE_ENV_SUFFIXES: Readonly<{
|
|
|
313
313
|
allowUpstreamEgress: "ALLOW_UPSTREAM_EGRESS";
|
|
314
314
|
compatOpencode: "COMPAT_OPENCODE";
|
|
315
315
|
noCompatPrompts: "NO_COMPAT_PROMPTS";
|
|
316
|
+
/**
|
|
317
|
+
* Advisory input-token budget for a managed session, set by the daemon from
|
|
318
|
+
* the server-issued `requestEnvelope.maxInputTokens` so the engine's
|
|
319
|
+
* auto-compaction can trigger before the relay refuses the request. The
|
|
320
|
+
* engine must treat it as a hint: enforcement stays server-side.
|
|
321
|
+
*/
|
|
322
|
+
maxInputTokens: "MAX_INPUT_TOKENS";
|
|
323
|
+
/**
|
|
324
|
+
* The session id the engine stamps into processes it spawns — the one
|
|
325
|
+
* variable on this table the engine WRITES rather than reads, so a child
|
|
326
|
+
* process (e.g. `skrr code handover --carry-session`) can name the session it
|
|
327
|
+
* is running inside without being told. Read with
|
|
328
|
+
* `readFirstPartyHarnessEngineEnv`: current namespace first, then legacy.
|
|
329
|
+
*/
|
|
330
|
+
sessionId: "SESSION_ID";
|
|
316
331
|
}>;
|
|
317
332
|
export type FirstPartyHarnessEngineEnvKey = keyof typeof ENGINE_ENV_SUFFIXES;
|
|
318
333
|
/**
|
|
@@ -350,7 +365,7 @@ export declare const FIRST_PARTY_HARNESS_ENGINE: Readonly<{
|
|
|
350
365
|
* `readFirstPartyHarnessEngineEnv` to read one the way the engine does and
|
|
351
366
|
* `firstPartyHarnessEngineEnvEntries` to set one every engine build honours.
|
|
352
367
|
*/
|
|
353
|
-
env: Readonly<Record<"appName" | "serverPassword" | "serverUsername" | "home" | "config" | "scriptName" | "allowUpstreamEgress" | "compatOpencode" | "noCompatPrompts", string>>;
|
|
368
|
+
env: Readonly<Record<"appName" | "serverPassword" | "serverUsername" | "home" | "config" | "scriptName" | "allowUpstreamEgress" | "compatOpencode" | "noCompatPrompts" | "maxInputTokens" | "sessionId", string>>;
|
|
354
369
|
}>;
|
|
355
370
|
/**
|
|
356
371
|
* Every name the engine reads for one purpose, current namespace first — the
|
|
@@ -450,6 +450,21 @@ const ENGINE_ENV_SUFFIXES = Object.freeze({
|
|
|
450
450
|
allowUpstreamEgress: 'ALLOW_UPSTREAM_EGRESS',
|
|
451
451
|
compatOpencode: 'COMPAT_OPENCODE',
|
|
452
452
|
noCompatPrompts: 'NO_COMPAT_PROMPTS',
|
|
453
|
+
/**
|
|
454
|
+
* Advisory input-token budget for a managed session, set by the daemon from
|
|
455
|
+
* the server-issued `requestEnvelope.maxInputTokens` so the engine's
|
|
456
|
+
* auto-compaction can trigger before the relay refuses the request. The
|
|
457
|
+
* engine must treat it as a hint: enforcement stays server-side.
|
|
458
|
+
*/
|
|
459
|
+
maxInputTokens: 'MAX_INPUT_TOKENS',
|
|
460
|
+
/**
|
|
461
|
+
* The session id the engine stamps into processes it spawns — the one
|
|
462
|
+
* variable on this table the engine WRITES rather than reads, so a child
|
|
463
|
+
* process (e.g. `skrr code handover --carry-session`) can name the session it
|
|
464
|
+
* is running inside without being told. Read with
|
|
465
|
+
* `readFirstPartyHarnessEngineEnv`: current namespace first, then legacy.
|
|
466
|
+
*/
|
|
467
|
+
sessionId: 'SESSION_ID',
|
|
453
468
|
});
|
|
454
469
|
function engineEnvName(appName, key) {
|
|
455
470
|
return `${appName.toUpperCase().replace(/-/g, '_')}_${ENGINE_ENV_SUFFIXES[key]}`;
|