@lanes-sh/link 0.2.0 → 0.2.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/package.json +1 -1
- package/src/cli/commands/operate/inspect.ts +7 -0
- package/src/cli/commands/operate/serve.ts +7 -0
- package/src/cli/commands/update.ts +255 -0
- package/src/cli/main.ts +4 -0
- package/src/cli/release.ts +111 -0
- package/src/cli/usage.ts +1 -0
- package/src/deployments/deploy.ts +7 -0
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
toPolicyDocument,
|
|
11
11
|
} from '#registry';
|
|
12
12
|
import { announce, emit, fail, ok, print, warn } from '../../output.ts';
|
|
13
|
+
import { staleNudge } from '../../release.ts';
|
|
13
14
|
import { capabilityDiff, discoveryProbe } from '../../runtime/discovery.ts';
|
|
14
15
|
import { openRuntime, resolveProfile, type GlobalFlags } from '../../runtime.ts';
|
|
15
16
|
|
|
@@ -189,6 +190,12 @@ export async function doctor(flags: DoctorFlags): Promise<void> {
|
|
|
189
190
|
});
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
// Every finding above is about this profile; this one is about the binary
|
|
194
|
+
// reading it. Silent when the registry cannot be reached — `doctor` is
|
|
195
|
+
// expected to work on a plane, and "could not check" is not a finding.
|
|
196
|
+
const stale = await staleNudge();
|
|
197
|
+
if (stale !== null) warnings.push({ kind: 'stale_release', message: stale });
|
|
198
|
+
|
|
192
199
|
await reportCapabilityDrift(runtime, (message) =>
|
|
193
200
|
warnings.push({ kind: 'capability_drift', message }),
|
|
194
201
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { startEndpoint } from '#server/endpoint.ts';
|
|
2
2
|
import { streamLogger } from '#server/logging.ts';
|
|
3
3
|
import { announce, ok, print, style, warn } from '../../output.ts';
|
|
4
|
+
import { staleNudge } from '../../release.ts';
|
|
4
5
|
import { resolveProfile, type GlobalFlags } from '../../runtime.ts';
|
|
5
6
|
|
|
6
7
|
/** `lanes link start` — reconcile, then serve every profile on one endpoint. */
|
|
@@ -41,6 +42,12 @@ export async function start(
|
|
|
41
42
|
|
|
42
43
|
print(ok(`serving ${style.bold(endpoint.url)}`));
|
|
43
44
|
print(style.dim(` profiles: ${endpoint.profiles.join(', ')}`));
|
|
45
|
+
|
|
46
|
+
// Last, not first: the endpoint is what someone ran this for, and a version
|
|
47
|
+
// note in front of it would be the first thing they read and the least useful.
|
|
48
|
+
const stale = await staleNudge();
|
|
49
|
+
if (stale !== null) print(warn(stale));
|
|
50
|
+
|
|
44
51
|
print(style.dim('Ctrl-C to stop.'));
|
|
45
52
|
|
|
46
53
|
const shutdown = async (): Promise<void> => {
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join, sep } from 'node:path';
|
|
3
|
+
import { installRoot } from '#profile';
|
|
4
|
+
import { emit, fail, ok, print, printErr, progress, style, warn } from '../output.ts';
|
|
5
|
+
import { PACKAGE, release, type ReleaseState } from '../release.ts';
|
|
6
|
+
import { version } from '../version.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `lanes link update` — install the newer release, or say why it will not.
|
|
10
|
+
*
|
|
11
|
+
* There is no build step and no compiled artifact, so updating means exactly
|
|
12
|
+
* one thing: replace the installed package directory with a newer tarball from
|
|
13
|
+
* the registry. `bin/lanes` resolves its own symlink chain and execs Bun on the
|
|
14
|
+
* `src/` inside that directory, so the shipped source *is* the running code and
|
|
15
|
+
* the symlink on the PATH never has to move.
|
|
16
|
+
*
|
|
17
|
+
* Bun is the only installer this drives. `bun install -g @lanes-sh/link` is the
|
|
18
|
+
* only install documented, `engines.bun` requires it, and the shim refuses to
|
|
19
|
+
* run without it — so inferring a package manager would be machinery serving a
|
|
20
|
+
* case nobody is told to create. The case that does exist is handled below
|
|
21
|
+
* rather than ignored: an `npm i -g` install updated with Bun gets a second
|
|
22
|
+
* copy somewhere else on the PATH, which this detects and reports instead of
|
|
23
|
+
* doing quietly.
|
|
24
|
+
*
|
|
25
|
+
* Nothing here is control plane — it touches the install, not a profile — so it
|
|
26
|
+
* resolves no profile and no target, and is the second command after `version`
|
|
27
|
+
* that prints no `announce` line.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export interface UpdateFlags {
|
|
31
|
+
/** Report and exit without installing anything. */
|
|
32
|
+
readonly check?: boolean | undefined;
|
|
33
|
+
readonly json?: boolean | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What `update` would do, and why.
|
|
38
|
+
*
|
|
39
|
+
* `'checkout'` is a refusal: `bun link` puts a checkout on the same PATH entry
|
|
40
|
+
* a published install would occupy, so installing from the registry there would
|
|
41
|
+
* leave two copies of this CLI and no indication of which one answers. `git
|
|
42
|
+
* pull` is the update in a checkout, and saying so is more useful than doing
|
|
43
|
+
* something surprising.
|
|
44
|
+
*/
|
|
45
|
+
export type UpdateAction = 'install' | 'current' | 'ahead' | 'checkout' | 'unknown';
|
|
46
|
+
|
|
47
|
+
export interface UpdateDecision {
|
|
48
|
+
readonly action: UpdateAction;
|
|
49
|
+
/** The argv to run, or `null` when nothing should be run. */
|
|
50
|
+
readonly install: readonly string[] | null;
|
|
51
|
+
readonly message: string;
|
|
52
|
+
/** Something true and unwelcome about this install, if there is anything. */
|
|
53
|
+
readonly warning: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UpdateInput {
|
|
57
|
+
readonly installed: string;
|
|
58
|
+
readonly latest: string | null;
|
|
59
|
+
readonly state: ReleaseState;
|
|
60
|
+
/** Where this CLI is installed — `installRoot()`, not the workspace. */
|
|
61
|
+
readonly root: string;
|
|
62
|
+
/** Where Bun keeps global installs, so a copy landing elsewhere is visible. */
|
|
63
|
+
readonly bunGlobal: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The whole decision, as a function of five strings.
|
|
68
|
+
*
|
|
69
|
+
* Split from the spawn because the alternative is a command whose only test is
|
|
70
|
+
* one that replaces the copy of this CLI on the machine running the suite. Every
|
|
71
|
+
* branch below is reachable from `update.test.ts` with no network and no
|
|
72
|
+
* subprocess — including the stale branch, which the checkout this is written in
|
|
73
|
+
* can never reach on its own, being by definition the newest thing there is.
|
|
74
|
+
*/
|
|
75
|
+
export function updatePlan(input: UpdateInput): UpdateDecision {
|
|
76
|
+
const { installed, latest, state, root, bunGlobal } = input;
|
|
77
|
+
|
|
78
|
+
// A published install lives under `node_modules`; a checkout does not. Cheaper
|
|
79
|
+
// and steadier than looking for `.git`, which a tarball could carry and a
|
|
80
|
+
// shallow export could lack.
|
|
81
|
+
const published = root.split(sep).includes('node_modules');
|
|
82
|
+
|
|
83
|
+
if (!published) {
|
|
84
|
+
return {
|
|
85
|
+
action: 'checkout',
|
|
86
|
+
install: null,
|
|
87
|
+
message: `${root} is a checkout, not an install — git pull is the update here`,
|
|
88
|
+
warning: null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (state === 'unknown') {
|
|
93
|
+
return {
|
|
94
|
+
action: 'unknown',
|
|
95
|
+
install: null,
|
|
96
|
+
message:
|
|
97
|
+
latest === null
|
|
98
|
+
? `could not reach the registry — ${installed} is installed`
|
|
99
|
+
: `cannot compare ${installed} against ${latest}`,
|
|
100
|
+
warning: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (state === 'ahead') {
|
|
105
|
+
return {
|
|
106
|
+
action: 'ahead',
|
|
107
|
+
install: null,
|
|
108
|
+
message: `${installed} is installed, ahead of the published ${latest ?? 'release'}`,
|
|
109
|
+
warning: null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (state === 'current') {
|
|
114
|
+
return { action: 'current', install: null, message: `${installed} is current`, warning: null };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Installed by npm, updated by Bun: `bun install -g` writes into its own
|
|
118
|
+
// global prefix and leaves the npm copy where it is, so both are on the PATH
|
|
119
|
+
// and its order decides which one answers. Worth saying before, not after.
|
|
120
|
+
const elsewhere = !root.startsWith(bunGlobal + sep);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
action: 'install',
|
|
124
|
+
install: ['install', '-g', PACKAGE],
|
|
125
|
+
message: `${installed} installed, ${latest} available`,
|
|
126
|
+
warning: elsewhere
|
|
127
|
+
? `this copy is at ${root}, which is not under ${bunGlobal} — ` +
|
|
128
|
+
'Bun will install a second copy there rather than replace this one, ' +
|
|
129
|
+
'and your PATH order decides which one answers'
|
|
130
|
+
: null,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Where Bun keeps global installs, honouring `BUN_INSTALL`. */
|
|
135
|
+
export function bunGlobalRoot(env: Record<string, string | undefined> = process.env): string {
|
|
136
|
+
return join(env['BUN_INSTALL'] ?? join(homedir(), '.bun'), 'install', 'global');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function update(flags: UpdateFlags): Promise<void> {
|
|
140
|
+
const current = await release();
|
|
141
|
+
const root = installRoot(import.meta.dir);
|
|
142
|
+
const decision = updatePlan({
|
|
143
|
+
installed: current.installed,
|
|
144
|
+
latest: current.latest,
|
|
145
|
+
state: current.state,
|
|
146
|
+
root,
|
|
147
|
+
bunGlobal: bunGlobalRoot(),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// A gate wants a non-zero exit for the one state that needs action. An
|
|
151
|
+
// unreachable registry is not that state — failing a build because a network
|
|
152
|
+
// was down would make this the flakiest check in it.
|
|
153
|
+
if (flags.check === true && decision.action === 'install') process.exitCode = 1;
|
|
154
|
+
|
|
155
|
+
const report = {
|
|
156
|
+
installed: current.installed,
|
|
157
|
+
latest: current.latest,
|
|
158
|
+
state: current.state,
|
|
159
|
+
action: decision.action,
|
|
160
|
+
root,
|
|
161
|
+
...(decision.install !== null ? { install: `bun ${decision.install.join(' ')}` } : {}),
|
|
162
|
+
...(decision.warning !== null ? { warning: decision.warning } : {}),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (flags.check === true || decision.action !== 'install') {
|
|
166
|
+
return emit(flags.json, report, () => {
|
|
167
|
+
if (decision.action === 'install') {
|
|
168
|
+
print(warn(decision.message));
|
|
169
|
+
if (decision.warning !== null) print(style.dim(` ${decision.warning}`));
|
|
170
|
+
print(style.dim(' run: lanes link update'));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Green for the two states that need nothing. A refusal and an
|
|
175
|
+
// unreachable registry are neither wrong nor fine, and `ok` would claim
|
|
176
|
+
// the second of those.
|
|
177
|
+
if (decision.action === 'current' || decision.action === 'ahead') {
|
|
178
|
+
print(ok(decision.message));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
print(style.dim(decision.message));
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Stderr, both of them. What this command produces is the version change, and
|
|
187
|
+
// with `--json` that is a document — a line of prose in front of it corrupts
|
|
188
|
+
// it for whatever is parsing, which is the whole reason `emit` exists.
|
|
189
|
+
if (decision.warning !== null) progress(warn(decision.warning));
|
|
190
|
+
progress(style.dim(`bun ${decision.install!.join(' ')}`));
|
|
191
|
+
|
|
192
|
+
const installed = await runInstall(decision.install!, flags.json === true);
|
|
193
|
+
if (!installed) {
|
|
194
|
+
printErr(fail('the install did not complete — nothing was changed'));
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Read the version back off disk rather than trusting the exit code. `version()`
|
|
200
|
+
// reads `package.json` from the install root at call time, so this is the one
|
|
201
|
+
// question worth asking after a successful install: did *this* copy change?
|
|
202
|
+
// Unchanged after a clean install is the second-copy case above, seen from the
|
|
203
|
+
// other side.
|
|
204
|
+
const landed = version();
|
|
205
|
+
|
|
206
|
+
return emit(
|
|
207
|
+
flags.json,
|
|
208
|
+
{
|
|
209
|
+
...report,
|
|
210
|
+
// The action was `install`; this is what came of it. Inventing a third
|
|
211
|
+
// action value would describe an outcome as a decision.
|
|
212
|
+
result: landed === current.installed ? 'unchanged' : 'installed',
|
|
213
|
+
installed: landed,
|
|
214
|
+
previous: current.installed,
|
|
215
|
+
},
|
|
216
|
+
() => {
|
|
217
|
+
if (landed === current.installed) {
|
|
218
|
+
print(warn(`bun reported success, but ${root} is still ${landed}`));
|
|
219
|
+
print(style.dim(' the copy it installed is somewhere else on your PATH'));
|
|
220
|
+
print(style.dim(' check with: which -a lanes'));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
print(ok(`${current.installed} → ${style.bold(landed)}`));
|
|
225
|
+
print(style.dim(' a running endpoint serves the old code until it is restarted'));
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Hand the install to Bun and let it own the terminal.
|
|
232
|
+
*
|
|
233
|
+
* `process.execPath` rather than `Bun.which('bun')`: this process is already
|
|
234
|
+
* running under the Bun that should do the installing, and a PATH lookup can
|
|
235
|
+
* find a different one — which would resolve the dependency set with a
|
|
236
|
+
* different resolver than the one that will run the result.
|
|
237
|
+
*
|
|
238
|
+
* Output is inherited rather than captured. Bun prints its own progress and its
|
|
239
|
+
* own errors, and paraphrasing a package manager's failure is how a report ends
|
|
240
|
+
* up less useful than the thing it replaced. Its stdout is dropped under
|
|
241
|
+
* `--json` for the same reason the lines above go to stderr: the document on
|
|
242
|
+
* stdout has to be the only thing on stdout. Its stderr is kept either way,
|
|
243
|
+
* because a failure is worth reading in both modes.
|
|
244
|
+
*/
|
|
245
|
+
async function runInstall(argv: readonly string[], json: boolean): Promise<boolean> {
|
|
246
|
+
try {
|
|
247
|
+
const child = Bun.spawn([process.execPath, ...argv], {
|
|
248
|
+
stdout: json ? 'ignore' : 'inherit',
|
|
249
|
+
stderr: 'inherit',
|
|
250
|
+
});
|
|
251
|
+
return (await child.exited) === 0;
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
package/src/cli/main.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
vaultRemove,
|
|
39
39
|
vaultSet,
|
|
40
40
|
} from './commands/owner.ts';
|
|
41
|
+
import { update } from './commands/update.ts';
|
|
41
42
|
import { globalFlags, ownerFlags, parseArgv, text } from './argv.ts';
|
|
42
43
|
import { PROGRAM, USAGE } from './usage.ts';
|
|
43
44
|
import { version } from './version.ts';
|
|
@@ -335,6 +336,9 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
335
336
|
print(version());
|
|
336
337
|
return;
|
|
337
338
|
|
|
339
|
+
case 'update':
|
|
340
|
+
return update({ check: flags['check'] === true, json });
|
|
341
|
+
|
|
338
342
|
default:
|
|
339
343
|
throw new Error(`Unknown command "${first}". Run: ${PROGRAM} help`);
|
|
340
344
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { version } from './version.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether a newer release than this one has been published.
|
|
5
|
+
*
|
|
6
|
+
* `version.ts` answers which release is installed, which stopped being the
|
|
7
|
+
* whole question when this started shipping from npm: two machines can now sit
|
|
8
|
+
* a release apart with nothing on either to say so, and the only upgrade
|
|
9
|
+
* affordance in the tree was a `contract` mismatch telling someone to "Upgrade
|
|
10
|
+
* lanes-link" without naming a command.
|
|
11
|
+
*
|
|
12
|
+
* Every function here degrades to `null` or `'unknown'` rather than throwing. A
|
|
13
|
+
* version check is never the reason a command fails — `doctor`, `start`, and
|
|
14
|
+
* `deploy` each print one line from this and must all work on a plane.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The npm package this CLI ships as, and the only thing `update` will install. */
|
|
18
|
+
export const PACKAGE = '@lanes-sh/link';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The dist-tags document, not the packument.
|
|
22
|
+
*
|
|
23
|
+
* `registry.npmjs.org/<name>` carries every version ever published with its
|
|
24
|
+
* full manifest — hundreds of kilobytes to answer a question whose answer is
|
|
25
|
+
* eighteen bytes. This endpoint returns `{"latest":"0.2.0"}` and nothing else.
|
|
26
|
+
*/
|
|
27
|
+
const DIST_TAGS = `https://registry.npmjs.org/-/package/${encodeURIComponent(PACKAGE)}/dist-tags`;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The same budget `endpointHealth` gives its probe.
|
|
31
|
+
*
|
|
32
|
+
* Long enough for a warm connection, short enough that a command which only
|
|
33
|
+
* mentions staleness in passing does not appear to hang on a captive-portal
|
|
34
|
+
* network that accepts the connection and then says nothing.
|
|
35
|
+
*/
|
|
36
|
+
const PROBE_TIMEOUT_MS = 700;
|
|
37
|
+
|
|
38
|
+
export type ReleaseState = 'current' | 'stale' | 'ahead' | 'unknown';
|
|
39
|
+
|
|
40
|
+
export interface Release {
|
|
41
|
+
readonly installed: string;
|
|
42
|
+
/** `null` when the registry could not be reached, or answered something else. */
|
|
43
|
+
readonly latest: string | null;
|
|
44
|
+
readonly state: ReleaseState;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* How the installed version stands against the published one.
|
|
49
|
+
*
|
|
50
|
+
* `'ahead'` is not a mistake: a contributor running from a checkout is usually
|
|
51
|
+
* a version ahead of the registry, and telling them they are behind would be
|
|
52
|
+
* both wrong and the thing they see most often.
|
|
53
|
+
*
|
|
54
|
+
* Pure, and separate from the fetch, so every branch is testable without a
|
|
55
|
+
* network — which is the only way the stale path gets covered at all, given the
|
|
56
|
+
* checkout this is written in is by definition current.
|
|
57
|
+
*/
|
|
58
|
+
export function releaseState(installed: string, latest: string | null): ReleaseState {
|
|
59
|
+
if (latest === null) return 'unknown';
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const order = Bun.semver.order(installed, latest);
|
|
63
|
+
return order === 0 ? 'current' : order < 0 ? 'stale' : 'ahead';
|
|
64
|
+
} catch {
|
|
65
|
+
// `Bun.semver.order` throws on anything it cannot parse rather than
|
|
66
|
+
// ordering it arbitrarily. A registry that answers with something other
|
|
67
|
+
// than a version, or a hand-edited `package.json`, is an unknown state and
|
|
68
|
+
// not a reason to claim either answer.
|
|
69
|
+
return 'unknown';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** What the registry calls `latest`, or `null` if it did not say. */
|
|
74
|
+
export async function latestRelease(): Promise<string | null> {
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(DIST_TAGS, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
77
|
+
if (!response.ok) return null;
|
|
78
|
+
|
|
79
|
+
const body = (await response.json()) as { latest?: unknown };
|
|
80
|
+
return typeof body.latest === 'string' ? body.latest : null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The installed version, the published one, and how they stand. */
|
|
87
|
+
export async function release(): Promise<Release> {
|
|
88
|
+
const installed = version();
|
|
89
|
+
const latest = await latestRelease();
|
|
90
|
+
|
|
91
|
+
return { installed, latest, state: releaseState(installed, latest) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The one line `doctor`, `start`, and `deploy` print when this install is behind.
|
|
96
|
+
*
|
|
97
|
+
* One string in one place, because three commands saying it three ways is how
|
|
98
|
+
* two of them end up naming a command that has been renamed. `null` for every
|
|
99
|
+
* state but `'stale'`: nothing is worth saying about an install that is current,
|
|
100
|
+
* and an unreachable registry is not news.
|
|
101
|
+
*/
|
|
102
|
+
export function staleLine(current: Release): string | null {
|
|
103
|
+
if (current.state !== 'stale') return null;
|
|
104
|
+
|
|
105
|
+
return `${current.installed} is installed, ${current.latest} is out — run: lanes link update`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `staleLine` over a fresh probe, for a caller that has no `Release` in hand. */
|
|
109
|
+
export async function staleNudge(): Promise<string | null> {
|
|
110
|
+
return staleLine(await release());
|
|
111
|
+
}
|
package/src/cli/usage.ts
CHANGED
|
@@ -90,6 +90,7 @@ ${style.bold('Inspection')}
|
|
|
90
90
|
${PROGRAM} audit verify has anything in the log been altered or removed
|
|
91
91
|
${PROGRAM} config show
|
|
92
92
|
${PROGRAM} version which release this is — same as lanes --version
|
|
93
|
+
${PROGRAM} update [--check] [--json] install the newer release, or say what is available
|
|
93
94
|
|
|
94
95
|
${style.bold('Attachments')}
|
|
95
96
|
${PROGRAM} attach <file> --connection <provider>.<account>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConfigError, resolveDeployTarget, type DeployConfig } from '#profile';
|
|
2
2
|
import { announce, fail, heading, ok, print, style, warn } from '#cli/output.ts';
|
|
3
|
+
import { staleNudge } from '#cli/release.ts';
|
|
3
4
|
import { confirm, isInteractive } from '#cli/prompt.ts';
|
|
4
5
|
import { openSecretStoreFor, resolveProfile, type GlobalFlags } from '#cli/runtime.ts';
|
|
5
6
|
import { resolveTarget, vaultEnv } from './bootstrap.ts';
|
|
@@ -57,6 +58,12 @@ export async function deploy(flags: DeployFlags): Promise<void> {
|
|
|
57
58
|
// rejected on boot should be rejected here, not after a five-minute build.
|
|
58
59
|
print(ok(`${resolution.profilePath} is valid`));
|
|
59
60
|
|
|
61
|
+
// The CLI planning this rollout, not the image it will build. An old one
|
|
62
|
+
// plans an old rollout, and a build is the most expensive place to find that
|
|
63
|
+
// out.
|
|
64
|
+
const stale = await staleNudge();
|
|
65
|
+
if (stale !== null) print(warn(stale));
|
|
66
|
+
|
|
60
67
|
const declared = await resolveTarget({
|
|
61
68
|
config,
|
|
62
69
|
profilePath: resolution.profilePath,
|