@uplink-code/cli 0.0.1 → 0.1.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/lib/cli/bin.js +15 -0
- package/lib/cli/index.js +1 -13
- package/lib/cli/src/bin.d.ts +2 -0
- package/lib/cli/src/commands/login.d.ts +7 -8
- package/lib/cli/src/commands/whoami.d.ts +6 -0
- package/lib/cli/src/credentials.d.ts +33 -3
- package/lib/cli/src/index.d.ts +8 -1
- package/lib/cli/src/refresh.d.ts +15 -0
- package/package.json +3 -3
- package/lib/cli/src/commands/session.d.ts +0 -14
package/lib/cli/bin.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var P=Object.defineProperty;var o=(e,n)=>P(e,"name",{value:n,configurable:!0});import{Command as M}from"commander";import{createServer as N}from"node:http";import{randomBytes as R}from"node:crypto";import{exec as T}from"node:child_process";import{homedir as v}from"node:os";import{join as m}from"node:path";import{mkdirSync as x,readFileSync as O,writeFileSync as E,unlinkSync as I,existsSync as g,chmodSync as A}from"node:fs";function k(){let e=process.env.XDG_CONFIG_HOME;return e?m(e,"uplink"):m(v(),".config","uplink")}o(k,"credentialsDir");function d(){return m(k(),"credentials.json")}o(d,"credentialsFile");function y(){let e=d();if(!g(e))return null;let n=O(e,"utf-8"),t;try{t=JSON.parse(n)}catch{return null}return $(t)?t:null}o(y,"readCredentials");function _(e){let n=k();x(n,{recursive:!0,mode:448}),E(d(),JSON.stringify(e,null,2),{mode:384}),A(d(),384)}o(_,"writeCredentials");function w(){let e=d();return g(e)?(I(e),!0):!1}o(w,"clearCredentials");function l(){return d()}o(l,"credentialsPath");function $(e){if(e===null||typeof e!="object")return!1;let n=e;return typeof n.access_token=="string"&&typeof n.refresh_token=="string"&&typeof n.expires_at=="number"&&typeof n.organization_id=="string"&&typeof n.supabase_url=="string"&&typeof n.supabase_anon_key=="string"}o($,"isCredentials");var L="https://console.uplink.build",b=300*1e3;async function h(){let e=process.env.UPLINK_CONSOLE_URL??L,n=R(16).toString("hex"),{port:t,waitForCallback:s,shutdown:i}=await J({state:n}),r=`${e}/cli/login?port=${t}&state=${encodeURIComponent(n)}`;process.stdout.write(`Opening browser to sign in\u2026
|
|
3
|
+
|
|
4
|
+
If nothing opens, visit:
|
|
5
|
+
${r}
|
|
6
|
+
|
|
7
|
+
`),z(r);try{let a=await s(),p={access_token:a.tokens.access_token,refresh_token:a.tokens.refresh_token,expires_at:a.tokens.expires_at,organization_id:a.organization_id,supabase_url:a.supabase_url,supabase_anon_key:a.supabase_anon_key};_(p),process.stdout.write(`\u2713 Signed in. Credentials saved to ${l()}
|
|
8
|
+
`)}finally{i()}}o(h,"login");function J(e){return new Promise((n,t)=>{let s,i,r=new Promise((c,f)=>{s=c,i=f}),a=N((c,f)=>{U(c,f,e.state,s,i)}),p=setTimeout(()=>{i(new Error(`Timed out after ${b/1e3}s waiting for sign-in. Re-run \`uplink login\`.`))},b);a.listen(0,"127.0.0.1",()=>{let c=a.address();n({port:c.port,waitForCallback:o(()=>r,"waitForCallback"),shutdown:o(()=>{clearTimeout(p),a.close()},"shutdown")})}),a.on("error",t)})}o(J,"startCallbackListener");async function U(e,n,t,s,i){if(n.setHeader("Access-Control-Allow-Origin","*"),n.setHeader("Access-Control-Allow-Methods","POST, OPTIONS"),n.setHeader("Access-Control-Allow-Headers","Content-Type"),e.method==="OPTIONS"){n.statusCode=204,n.end();return}if(e.method!=="POST"||e.url!=="/callback"){n.statusCode=404,n.end();return}try{let r=await F(e);if(!j(r))throw new Error("Malformed callback payload from console.");if(r.state!==t)throw new Error("State mismatch \u2014 refusing to accept tokens.");n.statusCode=200,n.end(JSON.stringify({ok:!0})),s({tokens:r.tokens,organization_id:r.organization_id,supabase_url:r.supabase_url,supabase_anon_key:r.supabase_anon_key})}catch(r){n.statusCode=400,n.end(JSON.stringify({ok:!1,error:r.message})),i(r instanceof Error?r:new Error(String(r)))}}o(U,"handleRequest");function F(e){return new Promise((n,t)=>{let s=[];e.on("data",i=>s.push(i)),e.on("end",()=>{try{n(JSON.parse(Buffer.concat(s).toString("utf-8")))}catch(i){t(i instanceof Error?i:new Error(String(i)))}}),e.on("error",t)})}o(F,"readJsonBody");function j(e){if(e===null||typeof e!="object")return!1;let n=e;if(typeof n.state!="string"||typeof n.organization_id!="string"||typeof n.supabase_url!="string"||typeof n.supabase_anon_key!="string")return!1;let t=n.tokens;if(t===null||typeof t!="object")return!1;let s=t;return typeof s.access_token=="string"&&typeof s.refresh_token=="string"&&typeof s.expires_at=="number"}o(j,"isCallbackPayload");function z(e){let n=process.platform==="darwin"?`open "${e}"`:process.platform==="win32"?`start "" "${e}"`:`xdg-open "${e}"`;T(n,t=>{})}o(z,"openInBrowser");function C(){w()?process.stdout.write(`\u2713 Removed ${l()}
|
|
9
|
+
`):process.stdout.write(`Already logged out.
|
|
10
|
+
`)}o(C,"logout");function S(){let e=y();e||(process.stdout.write("Not logged in. Run `uplink login`.\n"),process.exit(1));let n=H(e.access_token),t=n?.email??"(unknown)",s=n?.sub??"(unknown)",i=new Date(e.expires_at*1e3),r=e.expires_at*1e3<Date.now();process.stdout.write(`Email: ${t}
|
|
11
|
+
User id: ${s}
|
|
12
|
+
Expires: ${i.toISOString()}${r?" (expired \u2014 refresh on next call)":""}
|
|
13
|
+
Stored: ${l()}
|
|
14
|
+
`)}o(S,"whoami");function H(e){let n=e.split(".");if(n.length!==3)return null;try{let t=Buffer.from(n[1],"base64url").toString("utf-8");return JSON.parse(t)}catch{return null}}o(H,"decodeJwtPayload");var u=new M;u.name("uplink").description("CLI for the Uplink browser automation platform").version("0.0.1");u.command("login").description("Sign in via the browser and store credentials at ~/.config/uplink/credentials.json").action(()=>{h().catch(e=>{process.stderr.write(`Error: ${e.message}
|
|
15
|
+
`),process.exit(1)})});u.command("logout").description("Remove stored credentials").action(C);u.command("whoami").description("Show the currently signed-in user").action(S);u.parse();
|
package/lib/cli/index.js
CHANGED
|
@@ -1,13 +1 @@
|
|
|
1
|
-
|
|
2
|
-
var C=Object.defineProperty;var i=(e,t)=>C(e,"name",{value:t,configurable:!0});import{Command as L}from"commander";import{homedir as K}from"node:os";import{join as l}from"node:path";import{mkdirSync as A,readFileSync as P,writeFileSync as S,unlinkSync as I,existsSync as d,chmodSync as b}from"node:fs";var c=l(K(),".uplink"),n=l(c,"credentials.json"),u="https://api.uplink.build";function m(){if(!d(n))return null;let e=P(n,"utf-8"),t=JSON.parse(e);if(t===null||typeof t!="object"||typeof t.apiKey!="string")return null;let p=t;return{apiKey:p.apiKey??"",apiUrl:p.apiUrl??u}}i(m,"readCredentials");function f(e){A(c,{recursive:!0,mode:448}),S(n,JSON.stringify(e,null,2),{mode:384}),b(n,384)}i(f,"writeCredentials");function y(){return d(n)?(I(n),!0):!1}i(y,"clearCredentials");function r(){return n}i(r,"credentialsPath");function g(){return u}i(g,"defaultApiUrl");function k(e){e.apiKey||(process.stderr.write(`Error: --api-key is required.
|
|
3
|
-
|
|
4
|
-
Generate an API key in console (https://console.uplink.build \u2192 Settings \u2192 API Keys) and run:
|
|
5
|
-
uplink login --api-key <key>
|
|
6
|
-
`),process.exit(1)),f({apiKey:e.apiKey,apiUrl:e.apiUrl??g()}),process.stdout.write(`\u2713 Credentials written to ${r()}
|
|
7
|
-
`)}i(k,"login");function w(){y()?process.stdout.write(`\u2713 Removed ${r()}
|
|
8
|
-
`):process.stdout.write(`Already logged out.
|
|
9
|
-
`)}i(w,"logout");function x(){let e=m();e||(process.stdout.write("Not logged in. Run `uplink login --api-key <key>`.\n"),process.exit(1));let t=e.apiKey.length>8?`${e.apiKey.slice(0,4)}\u2026${e.apiKey.slice(-4)}`:"****";process.stdout.write(`API key: ${t}
|
|
10
|
-
API URL: ${e.apiUrl}
|
|
11
|
-
Stored: ${r()}
|
|
12
|
-
`)}i(x,"whoami");function s(e){process.stderr.write(`\`uplink session ${e}\` is not implemented in v0.0.1. Stay tuned.
|
|
13
|
-
`),process.exit(1)}i(s,"stub");function v(){s("start")}i(v,"start");function h(){s("list")}i(h,"list");function U(){s("resume")}i(U,"resume");var o=new L;o.name("uplink").description("CLI for the Uplink browser automation platform").version("0.0.1");o.command("login").description("Store API credentials at ~/.uplink/credentials.json").requiredOption("--api-key <key>","API key generated in console").option("--api-url <url>","Override the Atomic API base URL").action(e=>{k(e)});o.command("logout").description("Remove stored credentials").action(w);o.command("whoami").description("Show stored credentials (masked)").action(x);var a=o.command("session").description("Manage Uplink sessions (stubbed in v0.0.1)");a.command("start").description("Create a new session").action(v);a.command("list").description("List recent sessions").action(h);a.command("resume <id>").description("Reattach to an existing session").action(U);o.parse();
|
|
1
|
+
var u=Object.defineProperty;var n=(e,r)=>u(e,"name",{value:r,configurable:!0});import{homedir as f}from"node:os";import{join as o}from"node:path";import{mkdirSync as d,readFileSync as _,writeFileSync as k,unlinkSync as y,existsSync as c,chmodSync as g}from"node:fs";function p(){let e=process.env.XDG_CONFIG_HOME;return e?o(e,"uplink"):o(f(),".config","uplink")}n(p,"credentialsDir");function s(){return o(p(),"credentials.json")}n(s,"credentialsFile");var h="https://api.uplink.build";function i(){let e=s();if(!c(e))return null;let r=_(e,"utf-8"),t;try{t=JSON.parse(r)}catch{return null}return b(t)?t:null}n(i,"readCredentials");function a(e){let r=p();d(r,{recursive:!0,mode:448}),k(s(),JSON.stringify(e,null,2),{mode:384}),g(s(),384)}n(a,"writeCredentials");function m(){let e=s();return c(e)?(y(e),!0):!1}n(m,"clearCredentials");function x(){return s()}n(x,"credentialsPath");function C(e){return process.env.UPLINK_API_HOST??e?.api_host??h}n(C,"resolveApiHost");function b(e){if(e===null||typeof e!="object")return!1;let r=e;return typeof r.access_token=="string"&&typeof r.refresh_token=="string"&&typeof r.expires_at=="number"&&typeof r.organization_id=="string"&&typeof r.supabase_url=="string"&&typeof r.supabase_anon_key=="string"}n(b,"isCredentials");async function w(){let e=i();if(!e)throw new Error("No stored credentials. Run `uplink login` from @uplink-code/cli first.");let r=await fetch(`${e.supabase_url}/auth/v1/token?grant_type=refresh_token`,{method:"POST",headers:{"Content-Type":"application/json",apikey:e.supabase_anon_key},body:JSON.stringify({refresh_token:e.refresh_token})});if(!r.ok)throw new Error(`Token refresh failed: ${r.status} ${r.statusText}. Re-run \`uplink login\`.`);let t=await r.json();if(typeof t.access_token!="string"||typeof t.refresh_token!="string"||typeof t.expires_at!="number")throw new Error("Token refresh response was malformed.");let l={...e,access_token:t.access_token,refresh_token:t.refresh_token,expires_at:t.expires_at};return a(l),l}n(w,"refreshAccessToken");export{m as clearCredentials,x as credentialsPath,i as readCredentials,w as refreshAccessToken,C as resolveApiHost,a as writeCredentials};
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
export interface LoginOptions {
|
|
2
|
-
apiKey?: string;
|
|
3
|
-
apiUrl?: string;
|
|
4
|
-
}
|
|
5
1
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* `uplink login` — opens the browser to the console's `/cli/login`
|
|
3
|
+
* page, listens on an ephemeral localhost port for a POST containing
|
|
4
|
+
* the user's Supabase session, and writes the tokens to
|
|
5
|
+
* `~/.config/uplink/credentials.json`.
|
|
6
|
+
*
|
|
7
|
+
* Flow mirrors `gh auth login` / `gcloud auth login`.
|
|
9
8
|
*/
|
|
10
|
-
export declare function login(
|
|
9
|
+
export declare function login(): Promise<void>;
|
|
11
10
|
//# sourceMappingURL=login.d.ts.map
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uplink whoami` — decode the stored access_token (base64-url JSON, no
|
|
3
|
+
* signature verification — we trust the token because we stored it) and
|
|
4
|
+
* print identity + expiry. Returns non-zero exit if no credentials are
|
|
5
|
+
* stored.
|
|
6
|
+
*/
|
|
1
7
|
export declare function whoami(): void;
|
|
2
8
|
//# sourceMappingURL=whoami.d.ts.map
|
|
@@ -1,10 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens + endpoints needed to talk to the Uplink API and to refresh
|
|
3
|
+
* the Supabase access token.
|
|
4
|
+
*
|
|
5
|
+
* Shape mirrors a subset of `@supabase/supabase-js`'s `Session` plus
|
|
6
|
+
* the public Supabase project URL + anon key (so MCP can refresh the
|
|
7
|
+
* access token without an additional config step). All values written
|
|
8
|
+
* here are non-secret apart from the tokens themselves; the anon key
|
|
9
|
+
* is the same public key console ships to every browser.
|
|
10
|
+
*/
|
|
1
11
|
export interface Credentials {
|
|
2
|
-
|
|
3
|
-
|
|
12
|
+
access_token: string;
|
|
13
|
+
refresh_token: string;
|
|
14
|
+
/** Unix epoch seconds. */
|
|
15
|
+
expires_at: number;
|
|
16
|
+
/**
|
|
17
|
+
* Organization ID the user was operating under at login time. The
|
|
18
|
+
* `/sessions` POST needs this in its body because the token-auth
|
|
19
|
+
* middleware doesn't pick an org from the JWT alone; persisting at
|
|
20
|
+
* login time lets MCP send sessions without an extra round-trip.
|
|
21
|
+
*/
|
|
22
|
+
organization_id: string;
|
|
23
|
+
/** Public Supabase project URL — used for token refresh. */
|
|
24
|
+
supabase_url: string;
|
|
25
|
+
/** Public Supabase anon key — used as `apikey` header on refresh. */
|
|
26
|
+
supabase_anon_key: string;
|
|
27
|
+
/** Optional override for the Uplink API host (defaults to prod). */
|
|
28
|
+
api_host?: string;
|
|
4
29
|
}
|
|
5
30
|
export declare function readCredentials(): Credentials | null;
|
|
6
31
|
export declare function writeCredentials(creds: Credentials): void;
|
|
7
32
|
export declare function clearCredentials(): boolean;
|
|
8
33
|
export declare function credentialsPath(): string;
|
|
9
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the API host the MCP should hit. Env override wins over the
|
|
36
|
+
* value persisted in the credentials file; both win over the prod
|
|
37
|
+
* default.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveApiHost(creds: Credentials | null): string;
|
|
10
40
|
//# sourceMappingURL=credentials.d.ts.map
|
package/lib/cli/src/index.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Library entry — what other packages (notably @uplink-code/mcp) import
|
|
3
|
+
* from this CLI. Pure side-effect-free exports; the actual `uplink`
|
|
4
|
+
* binary lives in `./bin.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export { readCredentials, writeCredentials, clearCredentials, credentialsPath, resolveApiHost } from './credentials.ts';
|
|
7
|
+
export type { Credentials } from './credentials.ts';
|
|
8
|
+
export { refreshAccessToken } from './refresh.ts';
|
|
2
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Credentials } from './credentials.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Refresh the persisted access token via Supabase Auth's
|
|
4
|
+
* `/auth/v1/token?grant_type=refresh_token` endpoint, then write the
|
|
5
|
+
* new tokens back to the credentials file. Returns the refreshed
|
|
6
|
+
* `Credentials`.
|
|
7
|
+
*
|
|
8
|
+
* Callers (e.g. MCP) wrap this around any 401 response: catch, refresh,
|
|
9
|
+
* retry once.
|
|
10
|
+
*
|
|
11
|
+
* @throws if no credentials are stored, the refresh fails, or the
|
|
12
|
+
* response doesn't match the expected shape.
|
|
13
|
+
*/
|
|
14
|
+
export declare function refreshAccessToken(): Promise<Credentials>;
|
|
15
|
+
//# sourceMappingURL=refresh.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uplink-code/cli",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
}
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
|
-
"uplink": "./lib/cli/
|
|
13
|
+
"uplink": "./lib/cli/bin.js"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "npm run build:declarations && npm run build:transpile",
|
|
28
|
-
"build:transpile": "esbuild src/index.ts --bundle --keep-names --minify --format=esm --platform=node --external:commander --banner:js='#!/usr/bin/env node' --outfile=./lib/cli/
|
|
28
|
+
"build:transpile": "esbuild src/index.ts --bundle --keep-names --minify --format=esm --platform=node --outfile=./lib/cli/index.js && esbuild src/bin.ts --bundle --keep-names --minify --format=esm --platform=node --external:commander --banner:js='#!/usr/bin/env node' --outfile=./lib/cli/bin.js",
|
|
29
29
|
"build:declarations": "tsc -b",
|
|
30
30
|
"clean": "rm -rf lib",
|
|
31
31
|
"format:check": "prettier --check src/ package.json tsconfig.json",
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session commands — stubbed in v0.0.1.
|
|
3
|
-
*
|
|
4
|
-
* `start` will POST to the Atomic API to create a session and print
|
|
5
|
-
* an OTP + QR for device pairing. `list` will fetch the user's
|
|
6
|
-
* recent sessions. `resume` reattaches.
|
|
7
|
-
*
|
|
8
|
-
* Implementation lands once we have a shared Atomic API client
|
|
9
|
-
* (likely `@uplink-code/api-client` in this same monorepo).
|
|
10
|
-
*/
|
|
11
|
-
export declare function start(): void;
|
|
12
|
-
export declare function list(): void;
|
|
13
|
-
export declare function resume(): void;
|
|
14
|
-
//# sourceMappingURL=session.d.ts.map
|