@learncard/cli 3.6.7 → 4.0.0
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/CHANGELOG.md +79 -0
- package/README.md +36 -12
- package/dist/index.js +6673 -2038
- package/package.json +27 -15
- package/project.json +0 -42
- package/rollup.config.js +0 -34
- package/scripts/embed-snippets.ts +0 -47
- package/src/auth-grant.test.ts +0 -54
- package/src/auth-grant.ts +0 -34
- package/src/clr/validate.test.ts +0 -65
- package/src/clr/validate.ts +0 -242
- package/src/clr.ts +0 -119
- package/src/consent-contract.ts +0 -116
- package/src/demo-inbox-refresh.test.ts +0 -737
- package/src/demo-inbox-refresh.ts +0 -804
- package/src/demo-refresh-command.test.ts +0 -57
- package/src/demo-refresh-command.ts +0 -22
- package/src/demo-refresh-ui.test.ts +0 -66
- package/src/demo-refresh-ui.ts +0 -65
- package/src/demo-refresh.test.ts +0 -140
- package/src/demo-refresh.ts +0 -309
- package/src/doctor/checks.test.ts +0 -448
- package/src/doctor/checks.ts +0 -497
- package/src/doctor.test.ts +0 -67
- package/src/doctor.ts +0 -118
- package/src/embed.ts +0 -102
- package/src/inbox.test.ts +0 -257
- package/src/inbox.ts +0 -221
- package/src/index.tsx +0 -876
- package/src/init.ts +0 -26
- package/src/open.test.ts +0 -40
- package/src/open.ts +0 -186
- package/src/org/apply.test.ts +0 -1108
- package/src/org/apply.ts +0 -924
- package/src/org/branding.test.ts +0 -60
- package/src/org/diff.ts +0 -14
- package/src/org/load.ts +0 -50
- package/src/org/schema.test.ts +0 -256
- package/src/org/schema.ts +0 -216
- package/src/org.ts +0 -124
- package/src/out.test.ts +0 -40
- package/src/out.ts +0 -16
- package/src/phase-two.test.ts +0 -154
- package/src/project.test.ts +0 -232
- package/src/project.ts +0 -489
- package/src/promote.test.ts +0 -142
- package/src/promote.ts +0 -202
- package/src/random.ts +0 -3
- package/src/refresh.test.ts +0 -86
- package/src/refresh.ts +0 -93
- package/src/replHelpers.test.ts +0 -80
- package/src/replHelpers.ts +0 -45
- package/src/revoke.test.ts +0 -29
- package/src/revoke.ts +0 -65
- package/src/send-template.test.ts +0 -20
- package/src/send.test.ts +0 -321
- package/src/send.ts +0 -373
- package/src/setup-signing.test.ts +0 -62
- package/src/setup-signing.ts +0 -185
- package/src/snippet-files.ts +0 -16
- package/src/status.test.ts +0 -33
- package/src/status.ts +0 -94
- package/src/token.test.ts +0 -28
- package/src/token.ts +0 -139
- package/src/types.d.ts +0 -17
- package/src/verify.test.ts +0 -35
- package/src/verify.ts +0 -66
- package/src/webhook.ts +0 -247
- package/src/whoami.test.ts +0 -67
- package/src/whoami.ts +0 -129
- package/tsconfig.json +0 -39
- package/vitest.config.ts +0 -8
package/src/init.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
connect,
|
|
3
|
-
ensureIdentity,
|
|
4
|
-
ensureProfile,
|
|
5
|
-
loadProject,
|
|
6
|
-
type ProjectOptions,
|
|
7
|
-
} from './project';
|
|
8
|
-
import { out } from './out';
|
|
9
|
-
|
|
10
|
-
/** Create the identity and profile every other command needs, without sending anything. */
|
|
11
|
-
export const runInit = async (options: ProjectOptions): Promise<void> => {
|
|
12
|
-
const project = await loadProject(process.cwd());
|
|
13
|
-
const fresh = !project.env.SECURE_SEED;
|
|
14
|
-
const identity = await ensureIdentity(project, options);
|
|
15
|
-
const learnCard = await connect(project, options);
|
|
16
|
-
await ensureProfile(learnCard, identity, project);
|
|
17
|
-
out.set({
|
|
18
|
-
profileId: identity.profileId,
|
|
19
|
-
displayName: identity.displayName,
|
|
20
|
-
did: learnCard.id.did(),
|
|
21
|
-
created: fresh,
|
|
22
|
-
envPath: project.envPath,
|
|
23
|
-
});
|
|
24
|
-
out.log(fresh ? 'Ready. Your identity is in .env (keep it out of git).' : 'Already set up.');
|
|
25
|
-
out.log('Next: npx @learncard/cli send');
|
|
26
|
-
};
|
package/src/open.test.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { impliesNoBrowser, openPath, signInUrl } from './open';
|
|
3
|
-
import { appUrlFor, PRODUCTION_NETWORK, STAGING_NETWORK } from './project';
|
|
4
|
-
|
|
5
|
-
describe('open', () => {
|
|
6
|
-
it('maps networks to their app', () => {
|
|
7
|
-
expect(appUrlFor(PRODUCTION_NETWORK)).toBe('https://learncard.app');
|
|
8
|
-
expect(appUrlFor(STAGING_NETWORK)).toBe('https://staging.learncard.ai');
|
|
9
|
-
expect(appUrlFor('http://localhost:4000/trpc', 'http://localhost:3000/')).toBe(
|
|
10
|
-
'http://localhost:3000'
|
|
11
|
-
);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('needs the matching .env value for resource targets', () => {
|
|
15
|
-
expect(openPath('portal', {})).toBe('/app-store/developer');
|
|
16
|
-
expect(openPath('template', {})).toBeNull();
|
|
17
|
-
expect(openPath('template', { TEMPLATE_URI: 'lc:network:x/trpc:boost:1' })).toBe(
|
|
18
|
-
'/app-store/developer?template=lc%3Anetwork%3Ax%2Ftrpc%3Aboost%3A1'
|
|
19
|
-
);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('builds the sign-in URL with next as an app path, seed only in the fragment when asked', () => {
|
|
23
|
-
const plain = signInUrl('https://learncard.app', '/wallet');
|
|
24
|
-
expect(plain).toBe('https://learncard.app/developer/sign-in?next=%2Fwallet');
|
|
25
|
-
const withSeed = new URL(signInUrl('https://learncard.app', '/wallet', 'ab'.repeat(32)));
|
|
26
|
-
expect(withSeed.hash).toBe(`#seed=${'ab'.repeat(32)}`);
|
|
27
|
-
expect(withSeed.search).not.toContain('ab'.repeat(32));
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('never launches a browser when headless, --json, or --no-browser', () => {
|
|
31
|
-
// A real TTY with no flags: fine to launch.
|
|
32
|
-
expect(impliesNoBrowser({}, true)).toBe(false);
|
|
33
|
-
// --json always implies headless, even at a real TTY.
|
|
34
|
-
expect(impliesNoBrowser({ json: true }, true)).toBe(true);
|
|
35
|
-
// No TTY (piped/CI) implies headless regardless of flags.
|
|
36
|
-
expect(impliesNoBrowser({}, false)).toBe(true);
|
|
37
|
-
// Explicit --no-browser (commander's `browser: false`) always wins.
|
|
38
|
-
expect(impliesNoBrowser({ browser: false }, true)).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
});
|
package/src/open.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import clipboard from 'clipboardy';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
appUrlFor,
|
|
6
|
-
loadProject,
|
|
7
|
-
PRODUCTION_NETWORK,
|
|
8
|
-
resolveServices,
|
|
9
|
-
STAGING_NETWORK,
|
|
10
|
-
type ProjectOptions,
|
|
11
|
-
} from './project';
|
|
12
|
-
import { out } from './out';
|
|
13
|
-
|
|
14
|
-
export type OpenTarget = 'portal' | 'wallet' | 'template' | 'contract' | 'integration';
|
|
15
|
-
|
|
16
|
-
export const OPEN_TARGETS: Record<OpenTarget, string> = {
|
|
17
|
-
portal: 'the Developer Portal',
|
|
18
|
-
wallet: 'your wallet',
|
|
19
|
-
template: 'the template you last sent from',
|
|
20
|
-
contract: 'your consent contract',
|
|
21
|
-
integration: "your Claim button's integration",
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/** App path for a target, given the project's .env. Returns null when the .env lacks what the target needs. */
|
|
25
|
-
export const openPath = (target: OpenTarget, env: Record<string, string>): string | null => {
|
|
26
|
-
switch (target) {
|
|
27
|
-
case 'portal':
|
|
28
|
-
return '/app-store/developer';
|
|
29
|
-
case 'wallet':
|
|
30
|
-
return '/wallet';
|
|
31
|
-
case 'template':
|
|
32
|
-
return env.TEMPLATE_URI
|
|
33
|
-
? `/app-store/developer?template=${encodeURIComponent(env.TEMPLATE_URI)}`
|
|
34
|
-
: null;
|
|
35
|
-
case 'contract':
|
|
36
|
-
return env.CONTRACT_URI
|
|
37
|
-
? `/app-store/developer?contract=${encodeURIComponent(env.CONTRACT_URI)}`
|
|
38
|
-
: null;
|
|
39
|
-
case 'integration':
|
|
40
|
-
return env.INTEGRATION_ID
|
|
41
|
-
? `/app-store/developer/integrations/${encodeURIComponent(env.INTEGRATION_ID)}`
|
|
42
|
-
: null;
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export const signInUrl = (appUrl: string, next: string, seedInFragment?: string): string => {
|
|
47
|
-
const url = new URL('/developer/sign-in', appUrl);
|
|
48
|
-
url.searchParams.set('next', next);
|
|
49
|
-
if (seedInFragment) url.hash = `seed=${seedInFragment}`;
|
|
50
|
-
return url.toString();
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const CLIPBOARD_TTL_MS = 60_000;
|
|
54
|
-
|
|
55
|
-
/** Only plain http(s) URLs may reach the OS browser launcher or seed a sign-in link. */
|
|
56
|
-
const isHttpUrl = (value: string): boolean => {
|
|
57
|
-
try {
|
|
58
|
-
return ['http:', 'https:'].includes(new URL(value).protocol);
|
|
59
|
-
} catch {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const launchBrowser = (url: string): void => {
|
|
65
|
-
const [cmd, args] =
|
|
66
|
-
process.platform === 'darwin'
|
|
67
|
-
? ['open', [url]]
|
|
68
|
-
: process.platform === 'win32'
|
|
69
|
-
? // No shell involved: rundll32 receives the URL as a normal argv entry, so
|
|
70
|
-
// cmd.exe metacharacters (&, |, ^, etc.) in the URL are never interpreted.
|
|
71
|
-
['rundll32', ['url.dll,FileProtocolHandler', url]]
|
|
72
|
-
: ['xdg-open', [url]];
|
|
73
|
-
spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Clears the clipboard after the TTL, or immediately on Ctrl-C, but only if it still holds the
|
|
78
|
-
* seed we wrote (the user may have copied something else). Resolves once the clipboard is safe.
|
|
79
|
-
*/
|
|
80
|
-
const waitForClipboardClear = (seed: string): Promise<void> =>
|
|
81
|
-
new Promise(resolve => {
|
|
82
|
-
let settled = false;
|
|
83
|
-
const finish = async (): Promise<void> => {
|
|
84
|
-
if (settled) return;
|
|
85
|
-
settled = true;
|
|
86
|
-
clearTimeout(timer);
|
|
87
|
-
process.removeListener('SIGINT', onSigint);
|
|
88
|
-
if ((await clipboard.read().catch(() => '')) === seed)
|
|
89
|
-
await clipboard.write('').catch(() => {});
|
|
90
|
-
out.log('Clipboard cleared.');
|
|
91
|
-
resolve();
|
|
92
|
-
};
|
|
93
|
-
const onSigint = (): void => {
|
|
94
|
-
void finish();
|
|
95
|
-
};
|
|
96
|
-
const timer = setTimeout(() => void finish(), CLIPBOARD_TTL_MS);
|
|
97
|
-
process.once('SIGINT', onSigint);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
export interface OpenOptions extends ProjectOptions {
|
|
101
|
-
appUrl?: string;
|
|
102
|
-
urlFragment?: boolean;
|
|
103
|
-
/** commander's attribute name for `--no-browser`; false only when that flag is passed. */
|
|
104
|
-
browser?: boolean;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export type SeedDelivery = 'clipboard' | 'fragment' | 'none';
|
|
108
|
-
|
|
109
|
-
/** A script or `--json` run has nowhere to click "paste"; never launch a browser for it. */
|
|
110
|
-
export const impliesNoBrowser = (
|
|
111
|
-
options: { browser?: boolean; json?: boolean },
|
|
112
|
-
isTTY: boolean
|
|
113
|
-
): boolean => options.browser === false || !isTTY || !!options.json;
|
|
114
|
-
|
|
115
|
-
export const runOpen = async (
|
|
116
|
-
target: OpenTarget = 'portal',
|
|
117
|
-
options: OpenOptions
|
|
118
|
-
): Promise<void> => {
|
|
119
|
-
const project = await loadProject(process.cwd());
|
|
120
|
-
const seed = project.env.SECURE_SEED;
|
|
121
|
-
if (!seed)
|
|
122
|
-
throw new Error(
|
|
123
|
-
'No SECURE_SEED in .env. Run a command that creates one first, e.g. npx @learncard/cli send'
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const path = openPath(target, project.env);
|
|
127
|
-
if (!path)
|
|
128
|
-
throw new Error(
|
|
129
|
-
`Nothing saved for "${target}" yet. Run the command that creates it first (send --template, consent-contract, or embed).`
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
const { network } = resolveServices(project.env, options.network);
|
|
133
|
-
if (options.appUrl && !isHttpUrl(options.appUrl))
|
|
134
|
-
throw new Error('--app-url must be a valid http(s) URL.');
|
|
135
|
-
if (!options.appUrl && network !== PRODUCTION_NETWORK && network !== STAGING_NETWORK)
|
|
136
|
-
throw new Error(
|
|
137
|
-
`${network} has no hosted LearnCard app. Pass --app-url <url> for the app connected to it.`
|
|
138
|
-
);
|
|
139
|
-
const appUrl = appUrlFor(network, options.appUrl);
|
|
140
|
-
const noBrowser = impliesNoBrowser(options, !!process.stdout.isTTY);
|
|
141
|
-
|
|
142
|
-
let url: string;
|
|
143
|
-
let seedDelivery: SeedDelivery;
|
|
144
|
-
let clipboardCleared: Promise<void> | undefined;
|
|
145
|
-
if (options.urlFragment) {
|
|
146
|
-
url = signInUrl(appUrl, path, seed);
|
|
147
|
-
out.log(
|
|
148
|
-
'Warning: --url-fragment puts your seed in the browser URL (history, screenshots).'
|
|
149
|
-
);
|
|
150
|
-
seedDelivery = 'fragment';
|
|
151
|
-
} else if (out.json) {
|
|
152
|
-
// Non-interactive: nobody is there to paste, and blocking a script for the clipboard TTL
|
|
153
|
-
// would be worse than not copying. Scripts that need the seed use --url-fragment.
|
|
154
|
-
url = signInUrl(appUrl, path);
|
|
155
|
-
out.log(
|
|
156
|
-
'Seed not copied in --json mode. Paste it from .env, or re-run with --url-fragment.'
|
|
157
|
-
);
|
|
158
|
-
seedDelivery = 'none';
|
|
159
|
-
} else {
|
|
160
|
-
url = signInUrl(appUrl, path);
|
|
161
|
-
try {
|
|
162
|
-
await clipboard.write(seed);
|
|
163
|
-
out.log(`Copied your seed to the clipboard (clears in ${CLIPBOARD_TTL_MS / 1000}s).`);
|
|
164
|
-
seedDelivery = 'clipboard';
|
|
165
|
-
clipboardCleared = waitForClipboardClear(seed);
|
|
166
|
-
} catch {
|
|
167
|
-
out.log(
|
|
168
|
-
'Clipboard unavailable. Paste the seed from .env, or re-run with --url-fragment.'
|
|
169
|
-
);
|
|
170
|
-
seedDelivery = 'none';
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (!options.urlFragment) out.log('→ Click "Paste from clipboard", then Sign in.');
|
|
175
|
-
if (!noBrowser) launchBrowser(url);
|
|
176
|
-
// Printed last so `$(cli open | tail -1)` captures this line in non-json mode.
|
|
177
|
-
out.log(
|
|
178
|
-
`Opening ${OPEN_TARGETS[target]}: ${options.urlFragment ? url.split('#')[0] + '#seed=…' : url}`
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
out.set({ url, target, next: path, seedDelivery });
|
|
182
|
-
|
|
183
|
-
// Keep the process alive until the clipboard is actually cleared (or Ctrl-C clears it early);
|
|
184
|
-
// index.tsx's runCommand exits right after this promise resolves.
|
|
185
|
-
await clipboardCleared;
|
|
186
|
-
};
|