agmsg-cloud 0.1.0 → 0.1.2
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/scripts/version-output.js +65 -0
- package/dist/src/browser.js +24 -0
- package/dist/src/commands/login.js +39 -1
- package/dist/src/commands/sync.js +18 -3
- package/dist/src/preflight.js +19 -1
- package/package.json +3 -2
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The `--version` contract, parsed as records rather than searched for.
|
|
2
|
+
//
|
|
3
|
+
// The command answers with TWO facts now (#341) — which build, and which copy
|
|
4
|
+
// of it answered — and a check that takes the first line matching each shape
|
|
5
|
+
// cannot tell one answer from two disagreeing ones. A binary printing two
|
|
6
|
+
// identity lines is broken in a way that matters precisely when several
|
|
7
|
+
// installs are in play, which is the situation the second line was added for;
|
|
8
|
+
// `find` would report the first and call it a pass.
|
|
9
|
+
//
|
|
10
|
+
// So: exactly one of each, nothing else, and the version read from the identity
|
|
11
|
+
// line alone. That last part is not tidiness. A directory carries
|
|
12
|
+
// version-shaped fragments of its own — a node prefix, a store entry, an npx
|
|
13
|
+
// cache path — and scanning the whole output for "every version here is the one
|
|
14
|
+
// asked for" fails a CORRECT package the moment it is installed under such a
|
|
15
|
+
// path. A verification that fails good releases is worse than none: the next
|
|
16
|
+
// real failure gets waved through as another false alarm.
|
|
17
|
+
export const IDENTITY_PREFIX = 'agmsg-cloud/';
|
|
18
|
+
export const FROM_PREFIX = 'running from ';
|
|
19
|
+
const VERSION_SHAPED = /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g;
|
|
20
|
+
/** Distinct version-shaped tokens in one string, in order of first appearance. */
|
|
21
|
+
export function versionsIn(text) {
|
|
22
|
+
return [...new Set(text.match(VERSION_SHAPED) ?? [])];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read `--version` output as the pair of records it is.
|
|
26
|
+
*
|
|
27
|
+
* Every rejection names what was seen, because this runs against a published
|
|
28
|
+
* artifact where the next question is always "then what DID it print".
|
|
29
|
+
*/
|
|
30
|
+
export function parseVersionOutput(output) {
|
|
31
|
+
const lines = output
|
|
32
|
+
.split('\n')
|
|
33
|
+
.map((line) => line.trim())
|
|
34
|
+
.filter((line) => line !== '');
|
|
35
|
+
const identity = lines.filter((line) => line.startsWith(IDENTITY_PREFIX));
|
|
36
|
+
const from = lines.filter((line) => line.startsWith(FROM_PREFIX));
|
|
37
|
+
const other = lines.filter((line) => !line.startsWith(IDENTITY_PREFIX) && !line.startsWith(FROM_PREFIX));
|
|
38
|
+
if (identity.length === 0)
|
|
39
|
+
return { ok: false, reason: `no \`${IDENTITY_PREFIX}<version>\` line` };
|
|
40
|
+
if (identity.length > 1) {
|
|
41
|
+
return { ok: false, reason: `${identity.length} identity lines: ${identity.join(' | ')}` };
|
|
42
|
+
}
|
|
43
|
+
if (from.length === 0)
|
|
44
|
+
return { ok: false, reason: `no \`${FROM_PREFIX}<directory>\` line` };
|
|
45
|
+
if (from.length > 1) {
|
|
46
|
+
return { ok: false, reason: `${from.length} \`${FROM_PREFIX.trim()}\` lines: ${from.join(' | ')}` };
|
|
47
|
+
}
|
|
48
|
+
// Anything else is unexplained output, and this command's whole purpose is to
|
|
49
|
+
// be readable when something else has already gone wrong.
|
|
50
|
+
if (other.length > 0)
|
|
51
|
+
return { ok: false, reason: `unexpected output: ${other.join(' | ')}` };
|
|
52
|
+
const version = identity[0].slice(IDENTITY_PREFIX.length).trim();
|
|
53
|
+
if (version === '')
|
|
54
|
+
return { ok: false, reason: `identity line names no version: ${identity[0]}` };
|
|
55
|
+
// The identity line must carry ONE version and it must be the one it names —
|
|
56
|
+
// `agmsg-cloud/1.2.3 (from 4.5.6)` is two claims wearing one line.
|
|
57
|
+
const onLine = versionsIn(identity[0]);
|
|
58
|
+
if (onLine.length !== 1 || onLine[0] !== version) {
|
|
59
|
+
return { ok: false, reason: `identity line is not a single version: ${identity[0]}` };
|
|
60
|
+
}
|
|
61
|
+
const directory = from[0].slice(FROM_PREFIX.length).trim();
|
|
62
|
+
if (directory === '')
|
|
63
|
+
return { ok: false, reason: `no directory named: ${from[0]}` };
|
|
64
|
+
return { ok: true, version, directory };
|
|
65
|
+
}
|
package/dist/src/browser.js
CHANGED
|
@@ -90,6 +90,30 @@ export function verificationUrlIsSafe(url, endpoint) {
|
|
|
90
90
|
// operator who named their own endpoint owns their own topology.
|
|
91
91
|
return true;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* The console to send someone to, or null when we do not know one.
|
|
95
|
+
*
|
|
96
|
+
* Null is the answer for a self-hosted endpoint, and it is the whole reason
|
|
97
|
+
* this is a function rather than a constant anyone can print. The pin above
|
|
98
|
+
* exists because the official console's origin is a name we ship; a
|
|
99
|
+
* self-hosted deployment's is "not ours to know", and guessing `app.<their
|
|
100
|
+
* domain>` sends an operator to a service that may not be theirs — or may be
|
|
101
|
+
* someone else's.
|
|
102
|
+
*
|
|
103
|
+
* So a caller that wants to end a message with "and manage it at …" has to
|
|
104
|
+
* handle the null. That is deliberate: a message with no URL is worse than one
|
|
105
|
+
* with a wrong URL only until the wrong URL is followed.
|
|
106
|
+
*/
|
|
107
|
+
export function officialConsoleFor(endpoint) {
|
|
108
|
+
try {
|
|
109
|
+
return new URL(endpoint).origin === new URL(DEFAULT_ENDPOINT).origin
|
|
110
|
+
? OFFICIAL_CONSOLE_ORIGIN
|
|
111
|
+
: null;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
93
117
|
export function openUrl(url) {
|
|
94
118
|
const opener = openerFor(url);
|
|
95
119
|
return new Promise((resolve) => {
|
|
@@ -3,6 +3,8 @@ import { DEFAULT_ENDPOINT, armEnterToOpen, verificationUrlIsSafe, } from '../bro
|
|
|
3
3
|
import { credentialsForOrigin, isOrgAddress, originOf, writeCredential } from '../credentials.js';
|
|
4
4
|
import { settleMachineName, validateMachineName } from '../machine-name.js';
|
|
5
5
|
import { resolveScriptsDir } from '../config.js';
|
|
6
|
+
import { packageInstall } from '../version.js';
|
|
7
|
+
import { pathLookupCommand } from '../preflight.js';
|
|
6
8
|
import { teamsBoundTo } from '../oss.js';
|
|
7
9
|
// `login` — the device-authorization flow, from this machine's side.
|
|
8
10
|
//
|
|
@@ -360,7 +362,43 @@ async function pollUntilDecided(grant, ctx) {
|
|
|
360
362
|
// will look up again. Checked here so nothing durable happens first, and
|
|
361
363
|
// checked again in `keyFor` so a future caller cannot route around this.
|
|
362
364
|
if (!isOrgAddress(issued.org)) {
|
|
363
|
-
|
|
365
|
+
// WHAT THIS SENTENCE HAS TO GET RIGHT: whose fault it is, and what to
|
|
366
|
+
// do next. The previous wording — "the server answered with an org
|
|
367
|
+
// address this build does not recognise" — put the server first, and
|
|
368
|
+
// the person who hit it concluded the service was broken and stopped
|
|
369
|
+
// (#476). It was not broken: their machine was running a build from
|
|
370
|
+
// before `isOrgAddress` learned the second form.
|
|
371
|
+
//
|
|
372
|
+
// `credentials.ts` predicts this window in as many words: the server
|
|
373
|
+
// cutover "does not land until `npm view agmsg-cloud version` resolves
|
|
374
|
+
// to a build whose `isOrgAddress` accepts the new form". The window was
|
|
375
|
+
// designed for. What was never written was the sentence someone reads
|
|
376
|
+
// from inside it.
|
|
377
|
+
//
|
|
378
|
+
// The version AND the directory, from `packageInstall()`, which walks
|
|
379
|
+
// once and returns both. That pairing is the point here rather than a
|
|
380
|
+
// detail: the machine that hit this had TWO `agmsg-cloud` on PATH, and
|
|
381
|
+
// the one that answered `-v` was not the one that ran. A version alone
|
|
382
|
+
// would have been the same ambiguity in a new place. Reading the
|
|
383
|
+
// directory back against the shell's own lookup is what settles it.
|
|
384
|
+
//
|
|
385
|
+
// The lookup command comes from `preflight.ts`, which already had to
|
|
386
|
+
// decide it: `where` on Windows, `which` elsewhere. Printing `which`
|
|
387
|
+
// unconditionally would hand a Windows reader a broken command at the
|
|
388
|
+
// exact moment they have hit the PATH confusion this sentence exists to
|
|
389
|
+
// resolve. Raised in review, and taken from the module that RUNS it so
|
|
390
|
+
// the printed advice and the executed lookup cannot drift apart.
|
|
391
|
+
//
|
|
392
|
+
// No version number is named as the one to upgrade TO. Naming it needs
|
|
393
|
+
// a claim about which release first accepted this form, that claim
|
|
394
|
+
// decays every release, and a reader on that exact version is told to
|
|
395
|
+
// install what they already have. "Update, then run this again" is
|
|
396
|
+
// true whatever the answer is.
|
|
397
|
+
const { version, directory } = packageInstall();
|
|
398
|
+
throw new Error(`this build (${version}, from ${directory}) does not recognise the org address ` +
|
|
399
|
+
'the server sent — nothing was stored. Update with `npm i -g agmsg-cloud` and run ' +
|
|
400
|
+
'this command again. If the version above is not the one you expected, another ' +
|
|
401
|
+
`\`agmsg-cloud\` is ahead of it on PATH — compare it with \`${pathLookupCommand()} agmsg-cloud\`.`);
|
|
364
402
|
}
|
|
365
403
|
return finish(issued, endpoint, post, out);
|
|
366
404
|
}
|
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { hostname } from 'node:os';
|
|
3
3
|
import { CourierClient } from '../api.js';
|
|
4
4
|
import { originOf, readCredential } from '../credentials.js';
|
|
5
|
+
import { officialConsoleFor } from '../browser.js';
|
|
5
6
|
import { publicKeyOf } from '../oss.js';
|
|
6
7
|
import { deviceIdentityPath } from '../paths.js';
|
|
7
8
|
import { NEEDS, ensurePreflight, preflight } from '../preflight.js';
|
|
@@ -217,8 +218,22 @@ export async function cmdSync(config, opts) {
|
|
|
217
218
|
// authenticated, by machine.
|
|
218
219
|
await fetch(config, { team: opts.team });
|
|
219
220
|
// The last thing said, because "what now" is the question the screen leaves
|
|
220
|
-
// otherwise
|
|
221
|
-
// so is the point: someone who has just run five steps and watched a code
|
|
221
|
+
// otherwise: someone who has just run five steps and watched a code
|
|
222
222
|
// comparison has every reason to expect a sixth.
|
|
223
|
-
|
|
223
|
+
//
|
|
224
|
+
// "Nothing further to run" was true and was read as "nothing further to do",
|
|
225
|
+
// and the walk stopped there (#479). The two are different sentences. There
|
|
226
|
+
// is no sixth COMMAND, and there is somewhere to go — the console is where a
|
|
227
|
+
// team is managed once it syncs, and the CLI was the only thing that knew
|
|
228
|
+
// that and did not say it.
|
|
229
|
+
//
|
|
230
|
+
// The console is named only for the official endpoint. `browser.ts` pins that
|
|
231
|
+
// origin because it is a name we ship, and refuses to guess a self-hosted
|
|
232
|
+
// one — "its layout is not ours to know". Printing `app.<their domain>` would
|
|
233
|
+
// send an operator to a service that need not be theirs, so a self-hosted
|
|
234
|
+
// deployment gets the true half of the sentence and no invented URL.
|
|
235
|
+
const console_ = officialConsoleFor(config.baseUrl);
|
|
236
|
+
out(`\n"${opts.team}" is on this machine, unlocked and syncing. No further command to run` +
|
|
237
|
+
(console_ ? ` — manage it at ${console_}` : '') +
|
|
238
|
+
'.\n');
|
|
224
239
|
}
|
package/dist/src/preflight.js
CHANGED
|
@@ -97,11 +97,29 @@ function remoteSupportsConnect(scriptsDir) {
|
|
|
97
97
|
return /\bconnect\b/.test(text);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* What a person types to ask their shell which executable would run.
|
|
102
|
+
*
|
|
103
|
+
* `where` on Windows, `which` everywhere else. Exported and taking its platform
|
|
104
|
+
* as an argument because this answer is now needed in two places and both of
|
|
105
|
+
* them are wrong if they disagree: this module RUNS it, and `login` PRINTS it
|
|
106
|
+
* for an operator to run themselves (#476). Telling a Windows reader to run
|
|
107
|
+
* `which` hands them a broken command at the exact moment they have hit the
|
|
108
|
+
* PATH confusion the message exists to resolve — raised in review.
|
|
109
|
+
*
|
|
110
|
+
* A parameter rather than a module-level constant so both branches are
|
|
111
|
+
* reachable from a test on one machine. A constant bound at import time can
|
|
112
|
+
* only ever be checked on the platform the suite happens to run on, which is
|
|
113
|
+
* the branch nobody needed to check.
|
|
114
|
+
*/
|
|
115
|
+
export function pathLookupCommand(on = platform) {
|
|
116
|
+
return on === 'win32' ? 'where' : 'which';
|
|
117
|
+
}
|
|
100
118
|
function onPath(command) {
|
|
101
119
|
try {
|
|
102
120
|
// `command -v` through the shell would re-interpret the name; execFile with
|
|
103
121
|
// a fixed argv does not.
|
|
104
|
-
execFileSync(
|
|
122
|
+
execFileSync(pathLookupCommand(), [command], { stdio: 'ignore' });
|
|
105
123
|
return true;
|
|
106
124
|
}
|
|
107
125
|
catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agmsg-cloud",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Companion CLI for the agmsg cloud service: connect a team, join from another machine, and back up its keys.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist/src",
|
|
15
|
-
"
|
|
15
|
+
"dist/scripts",
|
|
16
|
+
"!dist/**/*.map"
|
|
16
17
|
],
|
|
17
18
|
"repository": {
|
|
18
19
|
"type": "git",
|