agmsg-cloud 0.1.0-rc.7 → 0.1.0-rc.8
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/src/commands/login.js +1 -1
- package/dist/src/commands/request.js +90 -2
- package/dist/src/commands/sync.js +39 -4
- package/dist/src/index.js +18 -3
- package/dist/src/recovery-key.js +13 -5
- package/dist/src/slot-advice.js +9 -3
- package/dist/src/version.js +21 -22
- package/package.json +3 -2
|
@@ -116,7 +116,7 @@ export async function cmdLogin(opts) {
|
|
|
116
116
|
if (stored && (asked === undefined || stored.machineName === asked)) {
|
|
117
117
|
const res = await post('/v1/device/activate', {}, stored.secret);
|
|
118
118
|
if (res.ok) {
|
|
119
|
-
out(`Already signed in as machine "${stored.machineName}".\n`);
|
|
119
|
+
out(`Already signed in as machine "${stored.machineName}" in organization "${stored.org}".\n`);
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
122
|
const code = await errorCode(res);
|
|
@@ -9,6 +9,72 @@ import { originOf } from '../credentials.js';
|
|
|
9
9
|
import { deviceIdentityPath } from '../paths.js';
|
|
10
10
|
import { closeAttempt, consumeAttempt, readBudget, renderBudgetExhausted, renderBudgetWarning, requesterLedgerScope, } from '../ledger.js';
|
|
11
11
|
import { attachRequestId, clearRecord, listResumableRequesterRecords, reserveRequesterNonce, } from '../pending.js';
|
|
12
|
+
/**
|
|
13
|
+
* The one thing a long wait cannot say for itself: that it is the design.
|
|
14
|
+
*
|
|
15
|
+
* `login` learned this in #279 — an agent held a URL and a code for 42 seconds
|
|
16
|
+
* waiting for a blocking command to return, because nothing told it the block
|
|
17
|
+
* WAS the mechanism. These waits are longer: `login` waits for a browser the
|
|
18
|
+
* operator already has open, and these wait for a person on another machine.
|
|
19
|
+
*
|
|
20
|
+
* Written once and used at every wait, so the three cannot drift into saying
|
|
21
|
+
* different things about the same behaviour. What may be added to it is what
|
|
22
|
+
* stopping costs, and that is NOT the same at every wait — see below.
|
|
23
|
+
*/
|
|
24
|
+
function saysItBlocks() {
|
|
25
|
+
return ' This command will not return until then. That is expected, not a failure.\n';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* What stopping costs, and it stops being free after the digits are shown.
|
|
29
|
+
*
|
|
30
|
+
* MEASURED, both of them, because the first version of this file asserted the
|
|
31
|
+
* resume in all three places and was wrong in one:
|
|
32
|
+
*
|
|
33
|
+
* interrupted before the answer the next run prints `resuming enrollment
|
|
34
|
+
* <id>` and continues the same ceremony.
|
|
35
|
+
* Budget unchanged: used:1 remaining:4.
|
|
36
|
+
*
|
|
37
|
+
* interrupted at the LAST wait, the row goes terminal while nothing is
|
|
38
|
+
* and the approver then answers watching. `GET /v1/enrollments` lists
|
|
39
|
+
* NONTERMINAL rows only, so the stored
|
|
40
|
+
* record matches nothing, the old attempt is
|
|
41
|
+
* closed as failed, and the next run starts a
|
|
42
|
+
* NEW ceremony. Measured: used:2 remaining:3
|
|
43
|
+
* — one of five, spent.
|
|
44
|
+
*
|
|
45
|
+
* So the resume line belongs at the first two waits and is false at the third.
|
|
46
|
+
* Saying it there would promise a cheap retry for the one interruption that
|
|
47
|
+
* costs something.
|
|
48
|
+
*/
|
|
49
|
+
function stoppingIsCheap() {
|
|
50
|
+
return ' Leave it running — if you do stop it, run the same command again to resume.\n';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* NOTHING HERE WARNS ABOUT A SECOND TERMINAL, and that is a refusal to guess
|
|
54
|
+
* rather than a finding.
|
|
55
|
+
*
|
|
56
|
+
* The console prompt says "do not start a second one in another terminal", and
|
|
57
|
+
* #344 asked whether that is true before repeating it. It was measured twice
|
|
58
|
+
* and the two measurements disagree:
|
|
59
|
+
*
|
|
60
|
+
* an attempt opened under a commitment no run could plan
|
|
61
|
+
* -> `consumeAttempt` refuses with `attempt_already_open`
|
|
62
|
+
* — but that is not the branch a second terminal takes,
|
|
63
|
+
* because `request` looks for a resumable record first.
|
|
64
|
+
* A rigged input, and it agreed with the warning.
|
|
65
|
+
*
|
|
66
|
+
* a first run interrupted mid-wait, then a second started
|
|
67
|
+
* -> once observed printing `resuming enrollment <id>`
|
|
68
|
+
* and carrying on at no cost; once observed starting a
|
|
69
|
+
* NEW ceremony instead. The two runs differed in their
|
|
70
|
+
* server double, and which difference decided it was
|
|
71
|
+
* not established.
|
|
72
|
+
*
|
|
73
|
+
* So this says nothing about second terminals in either direction. A warning
|
|
74
|
+
* has to be true to be worth a reader's attention, and so does a reassurance.
|
|
75
|
+
* What IS established is on `stoppingIsCheap` above: the cost of stopping, at
|
|
76
|
+
* each wait, measured.
|
|
77
|
+
*/
|
|
12
78
|
export async function cmdRequest(config, args, deps = {}) {
|
|
13
79
|
const out = deps.out ?? ((text) => void process.stdout.write(text));
|
|
14
80
|
const writeErr = deps.err ?? ((text) => void process.stderr.write(text));
|
|
@@ -179,14 +245,20 @@ export async function cmdRequest(config, args, deps = {}) {
|
|
|
179
245
|
}
|
|
180
246
|
const done = () => clearRecord({ role: 'requester', serverOrigin: config.baseUrl, key: record.commitmentHex }, env);
|
|
181
247
|
try {
|
|
182
|
-
out('
|
|
248
|
+
out('\nwaiting for an approver to commit...\n');
|
|
249
|
+
out(' The next move is on the other machine, where someone runs `agmsg-cloud approve`.\n');
|
|
250
|
+
out(saysItBlocks());
|
|
251
|
+
out(stoppingIsCheap());
|
|
183
252
|
await waitForStatus(client, requestId, 'both_committed', deps.waitOptions);
|
|
184
253
|
// Safe to open now: the approver's contribution is fixed and cannot change.
|
|
185
254
|
await client.submitOpening(requestId, {
|
|
186
255
|
device_pubkey: pubkey,
|
|
187
256
|
opening_nonce: record.nonceHex,
|
|
188
257
|
});
|
|
189
|
-
out('
|
|
258
|
+
out('\nwaiting for the approver to open...\n');
|
|
259
|
+
out(' Nothing to do here yet — the digits to compare appear when this returns.\n');
|
|
260
|
+
out(saysItBlocks());
|
|
261
|
+
out(stoppingIsCheap());
|
|
190
262
|
const opened = await waitForStatus(client, requestId, 'opened', deps.waitOptions);
|
|
191
263
|
// Derived here, from the two opened nonces. Nothing displayed below came
|
|
192
264
|
// from the server as a code.
|
|
@@ -205,6 +277,22 @@ export async function cmdRequest(config, args, deps = {}) {
|
|
|
205
277
|
// requirement that the requester counts failed and incomplete attempts, and
|
|
206
278
|
// warns from the second failure, did nothing on this side. Wait for the
|
|
207
279
|
// server to say which way it went.
|
|
280
|
+
// THE THIRD WAIT, and it said nothing at all — worse than the two the issue
|
|
281
|
+
// named, and the longest silence in the run.
|
|
282
|
+
//
|
|
283
|
+
// No line of its own, unlike the other two. What a reader has to do here is
|
|
284
|
+
// already on the screen directly above: `renderSasBlock` says the approving
|
|
285
|
+
// machine shows eight digits too, that they should check they are the same,
|
|
286
|
+
// and what each answer means. A copy of that here is the repetition #195
|
|
287
|
+
// removed from this very screen — a second copy of the one line that has to
|
|
288
|
+
// be read is how a reader learns to skim it. What was missing was not the
|
|
289
|
+
// instruction. It was that this blocks.
|
|
290
|
+
out('\nwaiting for the approver to answer...\n');
|
|
291
|
+
out(saysItBlocks());
|
|
292
|
+
// NOT `stoppingIsCheap()`. This is the one interruption that costs an
|
|
293
|
+
// attempt — see the note on that function for the measurement.
|
|
294
|
+
out(' Stopping here is the one that costs: if they answer while nothing is\n');
|
|
295
|
+
out(' watching, the next run starts over and spends one of five attempts.\n');
|
|
208
296
|
const settled = await waitForStatus(client, requestId, 'consumed', deps.waitOptions);
|
|
209
297
|
// What the eight digits authenticated, kept for `fetch`.
|
|
210
298
|
//
|
|
@@ -32,6 +32,39 @@ export async function cmdSync(config, opts) {
|
|
|
32
32
|
const request = d.request ?? cmdRequest;
|
|
33
33
|
const fetch = d.fetch ?? cmdFetch;
|
|
34
34
|
const pull = d.pull ?? cmdPull;
|
|
35
|
+
const client = new CourierClient(config);
|
|
36
|
+
// BEFORE THE CEREMONY, because `request` spends one of five attempts before
|
|
37
|
+
// it posts the enrollment. The ceremony is org-scoped and cannot discover
|
|
38
|
+
// that the named team belongs to another org; leaving this to `pull` made a
|
|
39
|
+
// wrong-account run finish the human comparison, spend an attempt, and only
|
|
40
|
+
// then say the team was absent (#345).
|
|
41
|
+
//
|
|
42
|
+
// This lookup is scoped by the current credential on the control plane. A
|
|
43
|
+
// failure to answer is not absence, and ambiguity is not resolved here:
|
|
44
|
+
// neither state is permission to spend a ceremony on a guessed team.
|
|
45
|
+
let matches;
|
|
46
|
+
try {
|
|
47
|
+
matches = opts.teamId === undefined
|
|
48
|
+
? await client.resolveTeamByName(opts.team)
|
|
49
|
+
: (await client.listTeams()).filter((team) => team.teamId === opts.teamId);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
out(`Not started: this account's organization could not be checked for "${opts.team}": ` +
|
|
53
|
+
`${err instanceof Error ? err.message : String(err)}\n` +
|
|
54
|
+
'No enrollment was started, so this costs none of your attempts.\n');
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (matches.length !== 1) {
|
|
59
|
+
out(matches.length === 0
|
|
60
|
+
? `Not started: this account's organization has no team named "${opts.team}".\n`
|
|
61
|
+
: `Not started: this account's organization has more than one team named "${opts.team}".\n`);
|
|
62
|
+
out('Check `agmsg-cloud whoami`, then sign in to the organization that holds the team.\n');
|
|
63
|
+
out('No enrollment was started, so this costs none of your attempts.\n');
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const resolvedTeamId = matches[0].teamId;
|
|
35
68
|
// The approver sees this, and they are looking for a machine they recognise.
|
|
36
69
|
// A hostname is what a person calls their laptop; a uuid is what they read
|
|
37
70
|
// aloud wrongly.
|
|
@@ -62,7 +95,7 @@ export async function cmdSync(config, opts) {
|
|
|
62
95
|
// join; failing the join over a failed courtesy check would be the refusal
|
|
63
96
|
// this deliberately is not.
|
|
64
97
|
try {
|
|
65
|
-
const existing = await
|
|
98
|
+
const existing = await client.listDevices();
|
|
66
99
|
// THIS MACHINE'S OWN ROW IS NOT A CLASH WITH ITSELF.
|
|
67
100
|
//
|
|
68
101
|
// The comparison was on label alone, so a machine already on the account —
|
|
@@ -162,9 +195,11 @@ export async function cmdSync(config, opts) {
|
|
|
162
195
|
// It stayed hidden because the second machine in testing pointed at the first
|
|
163
196
|
// machine's install, where the team was already present — so the second
|
|
164
197
|
// machine's path had never actually been walked.
|
|
165
|
-
await pull(config,
|
|
166
|
-
|
|
167
|
-
|
|
198
|
+
await pull(config, {
|
|
199
|
+
team: opts.team,
|
|
200
|
+
teamId: resolvedTeamId,
|
|
201
|
+
nextStepsFromCaller: true,
|
|
202
|
+
});
|
|
168
203
|
// And now the key, which opens what arrived above. Only reachable once the
|
|
169
204
|
// server says the ceremony was approved — `request` waits for that, and
|
|
170
205
|
// throws otherwise. The bundle is checked against the snapshot those digits
|
package/dist/src/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { cmdPull } from './commands/pull.js';
|
|
|
11
11
|
import { cmdRequest } from './commands/request.js';
|
|
12
12
|
import { cmdVaultPut, cmdVaultRestore } from './commands/vault.js';
|
|
13
13
|
import { cmdWatch } from './commands/watch.js';
|
|
14
|
-
import {
|
|
14
|
+
import { packageInstall } from './version.js';
|
|
15
15
|
const USAGE = `agmsg-cloud — hosted agmsg from this machine
|
|
16
16
|
|
|
17
17
|
login sign this machine in; approve it in your own browser
|
|
@@ -40,7 +40,8 @@ const USAGE = `agmsg-cloud — hosted agmsg from this machine
|
|
|
40
40
|
whoami which of this account's machines this one is —
|
|
41
41
|
its name, its organization, and the prefix the
|
|
42
42
|
console shows beside it
|
|
43
|
-
version the version of this CLI
|
|
43
|
+
version the version of this CLI, and the directory it is
|
|
44
|
+
running out of (also --version, -v)
|
|
44
45
|
the OSS scripts it drives are reported by
|
|
45
46
|
\`connect --preflight\`, which is a separate answer
|
|
46
47
|
|
|
@@ -104,7 +105,21 @@ async function main(argv) {
|
|
|
104
105
|
// one it must assume someone will paste. Written without naming a
|
|
105
106
|
// version: an example that has to be bumped is a second place to bump,
|
|
106
107
|
// which is the reason this command reads the manifest at all.
|
|
107
|
-
|
|
108
|
+
//
|
|
109
|
+
// The directory follows on its own line because the version alone does
|
|
110
|
+
// not answer the question people ask this command. `npm i -g agmsg-cloud`
|
|
111
|
+
// can succeed while a copy another package manager put earlier on PATH is
|
|
112
|
+
// what runs, and nothing about that is an error: both installs are valid
|
|
113
|
+
// and the shell picks one. That happened here, and the two defects being
|
|
114
|
+
// chased were already fixed in the copy that was NOT running. Choosing
|
|
115
|
+
// for the user would mean deciding their PATH; saying which one answered
|
|
116
|
+
// costs one line and leaves the choice where it was.
|
|
117
|
+
//
|
|
118
|
+
// A path, not a command: this line must not read as something to paste,
|
|
119
|
+
// which is why it is not shaped like an invocation.
|
|
120
|
+
const install = packageInstall();
|
|
121
|
+
process.stdout.write(`agmsg-cloud/${install.version}\n`);
|
|
122
|
+
process.stdout.write(`running from ${install.directory}\n`);
|
|
108
123
|
return;
|
|
109
124
|
}
|
|
110
125
|
// Beside `version` for the same reason it sits there: both answer a
|
package/dist/src/recovery-key.js
CHANGED
|
@@ -70,8 +70,8 @@ export function generateRecoveryKey() {
|
|
|
70
70
|
// error rather than a silent drop, so a wrong key fails as a wrong key and not
|
|
71
71
|
// as a mangled one.
|
|
72
72
|
export function normalizeRecoveryKey(input) {
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
const entered = input.trim();
|
|
74
|
+
let cleaned = entered
|
|
75
75
|
.toUpperCase()
|
|
76
76
|
.replace(/[\s-]/g, '');
|
|
77
77
|
// The prefix is a label, not key material, and it is stripped by LENGTH
|
|
@@ -87,6 +87,17 @@ export function normalizeRecoveryKey(input) {
|
|
|
87
87
|
if (cleaned.length === PREFIX_SYMBOLS + TOTAL_SYMBOLS && cleaned.startsWith('AGMSG')) {
|
|
88
88
|
cleaned = cleaned.slice(PREFIX_SYMBOLS);
|
|
89
89
|
}
|
|
90
|
+
// Locate a pasted label before diagnosing one character inside it. The
|
|
91
|
+
// prompt masks the input, so "unusable character: :" after a 50-character
|
|
92
|
+
// paste does not tell the operator where the colon was or what a key should
|
|
93
|
+
// look like. Length is the larger fact and can be checked without exposing
|
|
94
|
+
// the secret: the canonical form is 35 characters, while accepted spacing
|
|
95
|
+
// and grouping still reduce to exactly 25 key symbols (or 30 with AGMSG).
|
|
96
|
+
if (cleaned.length !== TOTAL_SYMBOLS) {
|
|
97
|
+
throw new Error(`that entry is ${entered.length} characters; a recovery key is 35 characters ` +
|
|
98
|
+
`in the form ${PREFIX}XXXXX-XXXXX-XXXXX-XXXXX-XXXXX ` +
|
|
99
|
+
`(${TOTAL_SYMBOLS} symbols without the prefix and separators)`);
|
|
100
|
+
}
|
|
90
101
|
cleaned = cleaned
|
|
91
102
|
.replace(/[IL]/g, '1')
|
|
92
103
|
.replace(/O/g, '0')
|
|
@@ -95,9 +106,6 @@ export function normalizeRecoveryKey(input) {
|
|
|
95
106
|
if (!ALPHABET.includes(ch))
|
|
96
107
|
throw new Error(`recovery key contains an unusable character: ${ch}`);
|
|
97
108
|
}
|
|
98
|
-
if (cleaned.length !== TOTAL_SYMBOLS) {
|
|
99
|
-
throw new Error(`recovery key must be ${TOTAL_SYMBOLS} symbols, got ${cleaned.length}`);
|
|
100
|
-
}
|
|
101
109
|
// The check symbol, verified here rather than at the vault. Without it, a
|
|
102
110
|
// mistyped key is indistinguishable from a wrong one until the AEAD refuses
|
|
103
111
|
// it — which is at restore time, the moment there is no way back. Naming the
|
package/dist/src/slot-advice.js
CHANGED
|
@@ -14,12 +14,18 @@ export function adviseOnSlot(result, ctx) {
|
|
|
14
14
|
switch (result.reason) {
|
|
15
15
|
case 'no-slot':
|
|
16
16
|
// The first backup on this machine, or the first after a re-issuance.
|
|
17
|
-
// Worth
|
|
18
|
-
//
|
|
17
|
+
// Worth saying before the next sentence asks for the recovery key. Do
|
|
18
|
+
// not promise "once": knowing that the slot is absent is not knowing
|
|
19
|
+
// that this session may write its replacement. A headless macOS session
|
|
20
|
+
// can read the default keychain name and still have add-generic-password
|
|
21
|
+
// refused with errSecInteractionNotAllowed. That fact is learned only
|
|
22
|
+
// after the vault has opened, when saveDeviceSlot performs the write.
|
|
19
23
|
return {
|
|
20
24
|
tone: 'note',
|
|
21
25
|
lines: [
|
|
22
|
-
'this machine has no key slot for this vault yet, so the recovery key is needed
|
|
26
|
+
'this machine has no key slot for this vault yet, so the recovery key is needed now.',
|
|
27
|
+
'After this succeeds, the CLI will try to keep a slot in the OS secure store. If this',
|
|
28
|
+
'session cannot use that store, the recovery key will be needed every time.',
|
|
23
29
|
],
|
|
24
30
|
};
|
|
25
31
|
case 'no-store':
|
package/dist/src/version.js
CHANGED
|
@@ -5,27 +5,23 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
// version — an identity, and the only thing that tells one package.json from
|
|
6
6
|
// another while walking up a tree that may contain several.
|
|
7
7
|
const PACKAGE_NAME = 'agmsg-cloud';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// stripped of its version is exactly when a monorepo root, an npx cache entry,
|
|
26
|
-
// or a parent workspace supplies its own. Reporting a stranger's version is the
|
|
27
|
-
// failure this command exists to prevent, wearing the shape of a success.
|
|
28
|
-
export function packageVersion(moduleUrl = import.meta.url) {
|
|
8
|
+
/**
|
|
9
|
+
* The install this process is running out of: its version AND where it is.
|
|
10
|
+
*
|
|
11
|
+
* Both come from ONE walk, and that is the point rather than a convenience.
|
|
12
|
+
* `which agmsg-cloud` answers from PATH and a manifest answers from the module
|
|
13
|
+
* graph; when two package managers have both installed this tool, those are two
|
|
14
|
+
* different questions with two different answers, and pairing them would report
|
|
15
|
+
* a directory that need not be where the version came from. The directory
|
|
16
|
+
* returned here is the one whose `package.json` supplied the version, so the
|
|
17
|
+
* two cannot disagree.
|
|
18
|
+
*
|
|
19
|
+
* Node resolves a module's realpath before loading it, so a bin symlink — the
|
|
20
|
+
* usual shape of a global install — has already been followed by the time
|
|
21
|
+
* `import.meta.url` exists. What comes back is the store directory, which is
|
|
22
|
+
* the part that tells a pnpm global install from an npm one.
|
|
23
|
+
*/
|
|
24
|
+
export function packageInstall(moduleUrl = import.meta.url) {
|
|
29
25
|
let dir = dirname(fileURLToPath(moduleUrl));
|
|
30
26
|
const { root } = parse(dir);
|
|
31
27
|
for (;;) {
|
|
@@ -41,7 +37,7 @@ export function packageVersion(moduleUrl = import.meta.url) {
|
|
|
41
37
|
if (manifest?.name === PACKAGE_NAME) {
|
|
42
38
|
const version = manifest.version;
|
|
43
39
|
if (typeof version === 'string' && version !== '')
|
|
44
|
-
return version;
|
|
40
|
+
return { version, directory: dir };
|
|
45
41
|
// Fail closed. This IS the package and it cannot say what it is; climbing
|
|
46
42
|
// past would hand the question to whatever sits above, which is how a
|
|
47
43
|
// broken install comes to announce a stranger's version.
|
|
@@ -55,3 +51,6 @@ export function packageVersion(moduleUrl = import.meta.url) {
|
|
|
55
51
|
// guessed — see the note above.
|
|
56
52
|
throw new Error(`cannot determine the installed version: no ${PACKAGE_NAME} package.json above this module`);
|
|
57
53
|
}
|
|
54
|
+
export function packageVersion(moduleUrl = import.meta.url) {
|
|
55
|
+
return packageInstall(moduleUrl).version;
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agmsg-cloud",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.8",
|
|
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",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
35
|
"lint": "tsx scripts/check-printed-commands.ts && tsx scripts/check-dist-is-current.ts && tsx scripts/check-readme-commands.ts && tsx scripts/check-handles.ts",
|
|
36
36
|
"prepack": "pnpm run build && tsx scripts/check-dist-is-current.ts && tsx scripts/check-readme-commands.ts && tsx scripts/check-handles.ts",
|
|
37
|
-
"verify:published": "tsx scripts/verify-published.ts"
|
|
37
|
+
"verify:published": "tsx scripts/verify-published.ts",
|
|
38
|
+
"verify:shown-install": "tsx scripts/verify-shown-install.ts"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@agmsg-cloud/sas-core": "workspace:*",
|