@portalsdk/cli 0.1.2 → 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.
Files changed (3) hide show
  1. package/README.md +49 -3
  2. package/dist/index.js +10 -5
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @portalsdk/cli
2
2
 
3
- The Portal command line. Two commands: deploy your configuration, and set secrets.
3
+ The Portal command line: sign in, deploy your configuration, and set secrets.
4
4
 
5
5
  ```bash
6
6
  npm install -g @portalsdk/cli
@@ -10,12 +10,58 @@ npx @portalsdk/cli deploy
10
10
 
11
11
  ## Authenticate
12
12
 
13
- Every command needs your project's **secret** key (`sk_...`):
13
+ There are two kinds of credential, for two kinds of command.
14
+
15
+ **Your account** — `portal login` signs you in as you, for commands that work
16
+ across your projects:
17
+
18
+ ```bash
19
+ portal login
20
+ ```
21
+
22
+ ```
23
+ Your code: K7WQ-3MTD
24
+ Approve at: https://app.useportal.co/authorize-cli?code=K7WQ3MTD
25
+
26
+ Waiting for approval…
27
+ ✓ Signed in as Sam Rivera (sam@example.com).
28
+ ```
29
+
30
+ Your browser opens to the approval page — confirm the code matches the one in
31
+ your terminal, and the CLI is signed in. The credential is stored in
32
+ `~/.portal/credentials.json`, readable only by you.
33
+
34
+ **A project key** — `deploy` and `secrets set` act on one environment, so they
35
+ take that environment's **secret** key (`sk_...`):
14
36
 
15
37
  ```bash
16
38
  export PORTAL_SECRET=sk_your_secret_key
17
39
  ```
18
40
 
41
+ ## `portal login`
42
+
43
+ Signs in to your Portal account. Options:
44
+
45
+ - `--name <name>` — label for this machine, so you can tell your devices apart
46
+ (default: its hostname).
47
+
48
+ ## `portal whoami`
49
+
50
+ Shows who you are signed in as.
51
+
52
+ ```bash
53
+ portal whoami
54
+ ```
55
+
56
+ ```
57
+ Sam Rivera (sam@example.com)
58
+ Acme
59
+ ```
60
+
61
+ ## `portal logout`
62
+
63
+ Signs out on this machine and revokes the credential.
64
+
19
65
  ## `portal deploy`
20
66
 
21
67
  Deploys the `portal.config.ts` in the current directory (or `--config <path>`).
@@ -60,7 +106,7 @@ portal secrets set OPENAI_API_KEY
60
106
 
61
107
  | Variable | Purpose |
62
108
  | --- | --- |
63
- | `PORTAL_SECRET` | Your project secret key (`sk_...`). Required. |
109
+ | `PORTAL_SECRET` | Your project secret key (`sk_...`). Required by `deploy` and `secrets set`. |
64
110
  | `PORTAL_API_URL` | Override the API base URL (for local development). Optional. |
65
111
 
66
112
  ## License
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import{Command as Pt}from"commander";import p from"picocolors";import ht from"path";var u=class extends Error{constructor(e,o,r){super(e);this.status=o;this.code=r;this.name="ApiError"}},d=class{constructor(n,e){this.secret=e;this.baseUrl=n.replace(/\/+$/,"")}baseUrl;async createDeploy(n){let{status:e,data:o}=await this.send("POST","/v1/deploys",n);if(e>=200&&e<300)return{versionId:D(o),alreadyDeployed:!1};if(e===409)return{versionId:D(o),alreadyDeployed:!0};throw this.fail(e,o)}async activate(n){let{status:e,data:o}=await this.send("POST",`/v1/deploys/${encodeURIComponent(n)}/activate`);if(e<200||e>=300)throw this.fail(e,o)}async setSecret(n,e){let{status:o,data:r}=await this.send("PUT",`/v1/secrets/${encodeURIComponent(n)}`,{value:e});if(o<200||o>=300)throw this.fail(o,r)}async send(n,e,o){let r;try{r=await fetch(`${this.baseUrl}${e}`,{method:n,headers:{authorization:`Bearer ${this.secret}`,"content-type":"application/json"},body:o===void 0?void 0:JSON.stringify(o)})}catch(i){throw new u(`Could not reach the Portal API at ${this.baseUrl}: ${i instanceof Error?i.message:String(i)}`,0)}let a=await r.text(),s={};if(a)try{s=JSON.parse(a)}catch{s={reason:a}}return{status:r.status,data:s}}fail(n,e){let o=e.code;return n===401||o==="unauthorized"?new u("Your PORTAL_SECRET was not accepted. It must be a valid project secret key (sk_...).",n,o):new u(e.reason||`The Portal API rejected the request (${o??n}).`,n,o)}};function D(t){if(!t.versionId)throw new u("The Portal API did not return a version id for this deploy.",502);return t.versionId}import{existsSync as I}from"fs";import k from"path";import{mkdtemp as V,rm as J,writeFile as Y}from"fs/promises";import{tmpdir as G}from"os";import g from"path";import{pathToFileURL as X}from"url";import M from"esbuild";var v=class extends Error{};async function x(t){let n;try{n=(await M.build({entryPoints:[t],bundle:!0,write:!1,format:"esm",platform:"node",target:"node18",logLevel:"silent",absWorkingDir:g.dirname(t)})).outputFiles[0].text}catch(r){throw new v(Q(r,t))}let e=await V(g.join(G(),"portal-cli-")),o=g.join(e,"module.mjs");try{return await Y(o,n,"utf8"),await import(X(o).href)}finally{await J(e,{recursive:!0,force:!0})}}function Q(t,n){if(t!=null&&typeof t=="object"&&"errors"in t&&Array.isArray(t.errors)){let o=M.formatMessagesSync(t.errors,{kind:"error",color:!1}).join(`
3
- `).trimEnd();if(o)return o}return`Failed to compile ${g.basename(n)}: ${t instanceof Error?t.message:String(t)}`}var m=class extends Error{},Z=["portal.config.ts","portal.config.mts","portal.config.js","portal.config.mjs"];function O(t,n){if(n){let e=k.resolve(t,n);if(!I(e))throw new m(`No config file found at ${n}`);return e}for(let e of Z){let o=k.join(t,e);if(I(o))return o}throw new m(`No portal.config.ts found in ${t}. Create one, or point at it with --config <path>.`)}async function R(t){let e=(await x(t)).default;if(e==null||typeof e!="object")throw new m(`${k.basename(t)} must \`export default defineConfig({ ... })\`.`);return e}function b(t,n){let e={project:{webhooks:null},channels:{}};t.auth&&(e.project.auth={issuer:t.auth.issuer,jwksUrl:t.auth.jwksUrl,claimMap:{...t.auth.claimMap}});for(let[o,r]of Object.entries(t.channels??{}))e.channels[o]=tt(r,n.get(o)??[]);return e}function tt(t,n){let e={};t.authz&&(e.authz=!0),t.onPublish&&t.onPublish.length>0&&(e.onPublish=t.onPublish.length),t.onDisconnect&&t.onDisconnect.length>0&&(e.onDisconnect=t.onDisconnect.length),t.notify&&(e.notify=!0);let o={};for(let r of n)o[r.handle]={namespace:r.namespace,transport:r.transport};return{mode:t.mode??"standard",anonymous:t.anonymous??!0,hooks:e,extensions:o}}import{mkdtemp as ot,rm as rt,writeFile as st}from"fs/promises";import{tmpdir as it}from"os";import y from"path";import at from"esbuild";var l=class extends Error{};function S(t,n){if(t.webhooks!=null)throw new l("The `webhooks` block is reserved for a future release. Remove it before deploying.");for(let[e,o]of Object.entries(t.channels??{})){if(o&&"onMiddlewareFailure"in o)throw new l(`Channel "${e}" sets \`onMiddlewareFailure\`, which is no longer supported \u2014 message handling always fails closed. Remove it.`);et(e),nt(e,n.get(e)??[])}}function et(t){if(t.includes("*")&&!/^[^*]+\*$/.test(t))throw new l(`Unsupported channel pattern "${t}": a template must end with a single "*" over a fixed prefix (e.g. "room-*").`)}function nt(t,n){let e=new Map;for(let o of n){let r=e.get(o.namespace);if(r)throw new l(`Channel "${t}" has two extensions using the namespace "${o.namespace}" ("${r}" and "${o.handle}"). Each namespace must be unique within a channel.`);e.set(o.namespace,o.handle)}}var ct="hooks";function N(t){return`import config from ${JSON.stringify(t)};
2
+ import{createRequire as tt}from"module";import{Command as nt}from"commander";import p from"picocolors";import{spawn as fe}from"child_process";import me from"os";var u=class extends Error{constructor(t,r,o){super(t);this.status=r;this.code=o;this.name="ApiError"}},x=class{constructor(n,t){this.secret=t;this.baseUrl=n.replace(/\/+$/,"")}baseUrl;async createDeploy(n){let{status:t,data:r}=await this.send("POST","/v1/deploys",n);if(t>=200&&t<300)return{versionId:K(r),alreadyDeployed:!1};if(t===409)return{versionId:K(r),alreadyDeployed:!0};throw this.fail(t,r)}async activate(n){let{status:t,data:r}=await this.send("POST",`/v1/deploys/${encodeURIComponent(n)}/activate`);if(t<200||t>=300)throw this.fail(t,r)}async setSecret(n,t){let{status:r,data:o}=await this.send("PUT",`/v1/secrets/${encodeURIComponent(n)}`,{value:t});if(r<200||r>=300)throw this.fail(r,o)}async send(n,t,r){let o;try{o=await fetch(`${this.baseUrl}${t}`,{method:n,headers:{authorization:`Bearer ${this.secret}`,"content-type":"application/json"},body:r===void 0?void 0:JSON.stringify(r)})}catch(a){throw new u(`Could not reach the Portal API at ${this.baseUrl}: ${a instanceof Error?a.message:String(a)}`,0)}let s=await o.text(),i={};if(s)try{i=JSON.parse(s)}catch{i={reason:s}}return{status:o.status,data:i}}fail(n,t){let r=t.code;return n===401||r==="unauthorized"?new u("Your PORTAL_SECRET was not accepted. It must be a valid project secret key (sk_...).",n,r):new u(t.reason||`The Portal API rejected the request (${r??n}).`,n,r)}};function K(e){if(!e.versionId)throw new u("The Portal API did not return a version id for this deploy.",502);return e.versionId}var f=class{baseUrl;constructor(n){this.baseUrl=n.replace(/\/+$/,"")}async startDeviceLogin(n){let{status:t,data:r}=await this.send("POST","/v1/device/start",{...n?{name:n}:{}});if(t<200||t>=300)throw this.fail(t,r);let o=r;if(!o.deviceCode||!o.userCode||!o.verificationUri)throw new u("The Portal API did not return a usable login code.",502);return{deviceCode:o.deviceCode,userCode:o.userCode,verificationUri:o.verificationUri,interval:typeof o.interval=="number"?o.interval:5,expiresIn:typeof o.expiresIn=="number"?o.expiresIn:600}}async pollDeviceLogin(n){let{status:t,data:r}=await this.send("POST","/v1/device/poll",{deviceCode:n});if(t===429)return{status:"slow_down"};if(t===404)return{status:"expired"};if(t<200||t>=300)throw this.fail(t,r);let o=r;switch(o.status){case"approved":return{status:"approved",token:o.token,expiresAt:o.expiresAt};case"denied":return{status:"denied"};case"expired":return{status:"expired"};default:return{status:"pending"}}}async fetchIdentity(n){let{status:t,data:r}=await this.send("GET","/v1/me",void 0,n);if(t<200||t>=300)throw this.fail(t,r);return r}async revokeCurrentToken(n){let{status:t,data:r}=await this.send("DELETE","/v1/cli-tokens/current",void 0,n);if(t<200||t>=300)throw this.fail(t,r)}async createProject(n,t,r){let{status:o,data:s}=await this.send("POST","/v1/projects",{organizationId:t,name:r},n);if(o<200||o>=300)throw this.fail(o,s);let i=s;if(!i.project?.id||!Array.isArray(i.environments))throw new u("The Portal API did not return a usable project.",502);return{project:i.project,environments:i.environments}}async createApiKey(n,t,r){let{status:o,data:s}=await this.send("POST",`/v1/environments/${encodeURIComponent(t)}/api-keys`,{keyType:r},n);if(o<200||o>=300)throw this.fail(o,s);let i=s;if(!i.apiKey?.value)throw new u("The Portal API did not return a usable API key.",502);return i.apiKey}async send(n,t,r,o){let s;try{s=await fetch(`${this.baseUrl}${t}`,{method:n,headers:{"content-type":"application/json",...o?{authorization:`Bearer ${o}`}:{}},body:r===void 0?void 0:JSON.stringify(r)})}catch(c){throw new u(`Could not reach the Portal API at ${this.baseUrl}: ${c instanceof Error?c.message:String(c)}`,0)}let i=await s.text(),a={};if(i)try{a=JSON.parse(i)}catch{a={reason:i}}return{status:s.status,data:a}}fail(n,t){let r=typeof t.code=="string"?t.code:void 0,o=typeof t.reason=="string"?t.reason:void 0;return n===401?new u("Your login is no longer valid.",n,r):new u(o||`The Portal API rejected the request (${r??n}).`,n,r)}};import v from"fs/promises";import k from"os";import _ from"path";var de=448,H=384;function z(e=k.homedir()){return _.join(e,".portal")}function A(e=k.homedir()){return _.join(z(e),"credentials.json")}async function E(e=k.homedir()){let n;try{n=await v.readFile(A(e),"utf8")}catch{return null}try{let t=JSON.parse(n);return typeof t.token!="string"||!t.token?null:{token:t.token,expiresAt:typeof t.expiresAt=="string"?t.expiresAt:void 0}}catch{return null}}async function W(e,n=k.homedir()){let t=z(n),r=A(n);return await v.mkdir(t,{recursive:!0,mode:de}),await v.writeFile(r,`${JSON.stringify(e,null,2)}
3
+ `,{mode:H}),await v.chmod(r,H),r}async function O(e=k.homedir()){try{await v.unlink(A(e))}catch{}}var ge=e=>new Promise(n=>setTimeout(n,e));function he(e){return e.length===8?`${e.slice(0,4)}-${e.slice(4)}`:e}function ye(){try{return me.hostname()||void 0}catch{return}}function we(e){let n=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",t=process.platform==="win32"?["/c","start","",e]:[e];try{let r=fe(n,t,{stdio:"ignore",detached:!0});r.on("error",()=>{}),r.unref()}catch{}}async function F(e){let n=e.sleep??ge,t=e.openBrowser??we,r=e.log??(l=>console.log(l)),o=new f(e.apiUrl),s=e.name??ye(),i=await o.startDeviceLogin(s),a=`${i.verificationUri}?code=${encodeURIComponent(i.userCode)}`;r(""),r(` Your code: ${he(i.userCode)}`),r(` Approve at: ${a}`),r(""),r(" Waiting for approval\u2026"),t(a);let c=Date.now()+i.expiresIn*1e3,d=i.interval*1e3;for(;Date.now()<c;){await n(d);let l=await o.pollDeviceLogin(i.deviceCode);if(l.status==="slow_down"){d+=5e3;continue}if(l.status==="pending")continue;if(l.status==="denied")throw new Error("The login request was denied.");if(l.status==="expired")throw new Error("The login request expired. Run `portal login` to try again.");if(!l.token)throw new Error("This login code was already used. Run `portal login` to start a new one.");let h=await W({token:l.token,expiresAt:l.expiresAt}),I=null;try{I=await o.fetchIdentity(l.token)}catch{I=null}return{identity:I,credentialsPath:h}}throw new Error("The login request expired. Run `portal login` to try again.")}async function B(e){let n=await E();if(!n)return{loggedIn:!1};try{return{loggedIn:!0,identity:await new f(e.apiUrl).fetchIdentity(n.token)}}catch(t){if(t instanceof u&&t.status===401)return await O(),{loggedIn:!1};throw t}}async function J(e){let n=await E();if(!n)return{wasLoggedIn:!1,revoked:!1};let t=!1;try{await new f(e.apiUrl).revokeCurrentToken(n.token),t=!0}catch{}return await O(),{wasLoggedIn:!0,revoked:t}}async function xe(){return(await E())?.token??null}async function S(){let e=await xe();if(!e)throw new Error("Not signed in. Run: portal login");return e}import _e from"path";import{existsSync as V}from"fs";import T from"path";import{mkdtemp as ve,rm as ke,writeFile as Pe}from"fs/promises";import{tmpdir as be}from"os";import C from"path";import{pathToFileURL as Ee}from"url";import Y from"esbuild";var U=class extends Error{};async function $(e){let n;try{n=(await Y.build({entryPoints:[e],bundle:!0,write:!1,format:"esm",platform:"node",target:"node18",logLevel:"silent",absWorkingDir:C.dirname(e)})).outputFiles[0].text}catch(o){throw new U(Se(o,e))}let t=await ve(C.join(be(),"portal-cli-")),r=C.join(t,"module.mjs");try{return await Pe(r,n,"utf8"),await import(Ee(r).href)}finally{await ke(t,{recursive:!0,force:!0})}}function Se(e,n){if(e!=null&&typeof e=="object"&&"errors"in e&&Array.isArray(e.errors)){let r=Y.formatMessagesSync(e.errors,{kind:"error",color:!1}).join(`
4
+ `).trimEnd();if(r)return r}return`Failed to compile ${C.basename(n)}: ${e instanceof Error?e.message:String(e)}`}var P=class extends Error{},Ce=["portal.config.ts","portal.config.mts","portal.config.js","portal.config.mjs"];function G(e,n){if(n){let t=T.resolve(e,n);if(!V(t))throw new P(`No config file found at ${n}`);return t}for(let t of Ce){let r=T.join(e,t);if(V(r))return r}throw new P(`No portal.config.ts found in ${e}. Create one, or point at it with --config <path>.`)}async function X(e){let t=(await $(e)).default;if(t==null||typeof t!="object")throw new P(`${T.basename(e)} must \`export default defineConfig({ ... })\`.`);return t}function D(e,n){let t={project:{webhooks:null},channels:{}};e.auth&&(t.project.auth={issuer:e.auth.issuer,jwksUrl:e.auth.jwksUrl,claimMap:{...e.auth.claimMap}});for(let[r,o]of Object.entries(e.channels??{}))t.channels[r]=$e(o,n.get(r)??[]);return t}function $e(e,n){let t={};e.authz&&(t.authz=!0),e.onPublish&&e.onPublish.length>0&&(t.onPublish=e.onPublish.length),e.onDisconnect&&e.onDisconnect.length>0&&(t.onDisconnect=e.onDisconnect.length),e.notify&&(t.notify=!0);let r={};for(let o of n)r[o.handle]={namespace:o.namespace,transport:o.transport};return{mode:e.mode??"standard",anonymous:e.anonymous??!0,hooks:t,extensions:r}}import{mkdtemp as Ae,rm as Oe,writeFile as Ue}from"fs/promises";import{tmpdir as Te}from"os";import j from"path";import De from"esbuild";var g=class extends Error{};function L(e,n){if(e.webhooks!=null)throw new g("The `webhooks` block is reserved for a future release. Remove it before deploying.");for(let[t,r]of Object.entries(e.channels??{})){if(r&&"onMiddlewareFailure"in r)throw new g(`Channel "${t}" sets \`onMiddlewareFailure\`, which is no longer supported \u2014 message handling always fails closed. Remove it.`);je(t),Ie(t,n.get(t)??[])}}function je(e){if(e.includes("*")&&!/^[^*]+\*$/.test(e))throw new g(`Unsupported channel pattern "${e}": a template must end with a single "*" over a fixed prefix (e.g. "room-*").`)}function Ie(e,n){let t=new Map;for(let r of n){let o=t.get(r.namespace);if(o)throw new g(`Channel "${e}" has two extensions using the namespace "${r.namespace}" ("${o}" and "${r.handle}"). Each namespace must be unique within a channel.`);t.set(r.namespace,r.handle)}}var Le="hooks";function Z(e){return`import config from ${JSON.stringify(e)};
4
5
 
5
6
  const channels = config.channels || {};
6
7
 
@@ -49,6 +50,10 @@ export async function run(request) {
49
50
  }
50
51
 
51
52
  export default { run };
52
- `}function T(t){return Object.values(t.channels??{}).some(n=>n.authz!=null||(n.onPublish?.length??0)>0||(n.onDisconnect?.length??0)>0||n.notify!=null)}async function P(t,n,e){let o=y.dirname(n),r;if(T(t)){let s=await pt(n);r={name:ct,content:s}}let a=[];for(let[s,i]of e)for(let c of i){let f=await q(c.path,o);a.push({key:s,handle:c.handle,name:ft(s,c.handle),content:f})}return lt(a),{hookScript:r,extensionScripts:a}}function lt(t){let n=new Map;for(let e of t){let o=n.get(e.name);if(o)throw new l(`Extensions "${o.handle}" (channel "${o.key}") and "${e.handle}" (channel "${e.key}") both map to the upload name "${e.name}". Rename a handle or channel so they differ by more than punctuation.`);n.set(e.name,e)}}function C(t,n){for(let e of n){let o=t.channels[e.key]?.extensions[e.handle];o&&(o.script=e.name)}}async function pt(t){let n=await ot(y.join(it(),"portal-cli-hooks-")),e=y.join(n,"hooks.mjs");try{return await st(e,N(t),"utf8"),await q(e,y.dirname(t))}finally{await rt(n,{recursive:!0,force:!0})}}async function q(t,n){return(await at.build({entryPoints:[t],bundle:!0,write:!1,format:"esm",target:"es2022",platform:"neutral",logLevel:"silent",absWorkingDir:n})).outputFiles[0].text}function ft(t,n){return`ext-${U(t)}-${U(n)}`}function U(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}import{existsSync as ut}from"fs";import dt from"path";var h=class extends Error{};async function $(t,n){let e=new Map;for(let[o,r]of Object.entries(t.channels??{})){let a=r.extensions;if(!a||Object.keys(a).length===0)continue;let s=[];for(let[i,c]of Object.entries(a)){let f=dt.resolve(n,c);if(!ut(f))throw new h(`Channel "${o}" attaches extension "${i}" from "${c}", but that file does not exist.`);let K=await x(f),E=mt(K.default);if(!E)throw new h(`Extension "${i}" (${c}) must \`export default defineExtension(...)\` with a static manifest.`);s.push({handle:i,source:c,namespace:E.namespace,transport:E.transport,path:f})}e.set(o,s)}return e}function mt(t){if(typeof t!="function"&&typeof t!="object")return null;let n=t?.manifest;if(n==null||typeof n!="object")return null;let{namespace:e,transport:o}=n;return typeof e!="string"||!e.endsWith(".")||o!=="ws"&&o!=="http"?null:{namespace:e,transport:o}}async function H(t){let n=O(t.cwd,t.configPath),e=await R(n),o=ht.dirname(n),r=await $(e,o);S(e,r);let a=b(e,r),s=await P(e,n,r);C(a,s.extensionScripts);let i=new d(t.apiUrl,t.secret),{versionId:c,alreadyDeployed:f}=await i.createDeploy({extract:a,hookScript:s.hookScript,extensionScripts:s.extensionScripts.length>0?s.extensionScripts:void 0});return await i.activate(c),{configPath:n,versionId:c,alreadyDeployed:f,channels:Object.keys(a.channels),hasHookScript:!!s.hookScript,extensionCount:s.extensionScripts.length}}async function L(t){await new d(t.apiUrl,t.secret).setSecret(t.name,t.value)}import F from"path";var gt="https://api.useportal.co";function _(t,n){let e=F.relative(n,t);return e&&!e.startsWith("..")&&!F.isAbsolute(e)?e:t}function j(){return(process.env.PORTAL_API_URL||gt).replace(/\/+$/,"")}function A(){let t=process.env.PORTAL_SECRET;if(!t)throw new Error("Set PORTAL_SECRET to your project's secret key (sk_...) before running this command.");return t}async function B(t){if(t!=null&&t!=="")return t;if(process.stdin.isTTY){let e=await St("Value: ");if(!e)throw new Error("No value entered.");return e}let n=await xt();if(!n)throw new Error("No value provided. Pass --value <value> or pipe the value on stdin.");return n}async function xt(){let t=[];for await(let n of process.stdin)t.push(n);return Buffer.concat(t).toString("utf8").trim()}var yt=10,wt=13,Et=4,vt=3,kt=8,bt=127;function St(t){return new Promise(n=>{let e=process.stdin;process.stdout.write(t),e.setRawMode?.(!0),e.resume(),e.setEncoding("utf8");let o="",r=a=>{for(let s of a){let i=s.charCodeAt(0);if(i===yt||i===wt||i===Et){e.setRawMode?.(!1),e.pause(),e.removeListener("data",r),process.stdout.write(`
53
- `),n(o);return}i===vt&&(e.setRawMode?.(!1),e.pause(),e.removeListener("data",r),process.stdout.write(`
54
- `),process.exit(1)),i===kt||i===bt?o=o.slice(0,-1):o+=s}};e.on("data",r)})}var Ct="0.1.0",w=new Pt;w.name("portal").description("Deploy your Portal configuration and manage secrets.").version(Ct);w.command("deploy").description("Deploy your portal.config.ts.").option("-c, --config <path>","Path to your config file (default: portal.config.ts)").action(async t=>{await W(async()=>{let n=A(),e=await H({cwd:process.cwd(),configPath:t.config,apiUrl:j(),secret:n});jt(e)})});var $t=w.command("secrets").description("Manage your project secrets.");$t.command("set").argument("<name>",'Secret name, referenced in your config with env("NAME")').description("Set a project secret.").option("--value <value>","The secret value (otherwise read from stdin)").action(async(t,n)=>{await W(async()=>{let e=A(),o=await B(n.value);await L({name:t,value:o,apiUrl:j(),secret:e}),console.log(`${p.green("\u2713")} Secret ${p.bold(t)} set.`)})});w.parseAsync(process.argv);async function W(t){try{await t()}catch(n){console.error(`${p.red("Error:")} ${n instanceof Error?n.message:String(n)}`),process.exitCode=1}}function jt(t){t.alreadyDeployed?console.log(`${p.green("\u2713")} Already up to date \u2014 version ${p.bold(t.versionId)}`):(console.log(`${p.green("\u2713")} Deployed ${p.bold(_(t.configPath,process.cwd()))}`),console.log(` Version ${t.versionId}`));let{channels:n}=t;console.log(` ${n.length} channel ${z(n.length,"override","overrides")}: ${n.join(", ")}`);let e=[];t.hasHookScript&&e.push("hooks"),t.extensionCount>0&&e.push(`${t.extensionCount} ${z(t.extensionCount,"extension","extensions")}`),e.length>0&&console.log(` Uploaded: ${e.join(", ")}`),console.log(p.dim(" Channels with active connections keep their current configuration until they restart; new connections use this version right away."))}function z(t,n,e){return t===1?n:e}
53
+ `}function ee(e){return Object.values(e.channels??{}).some(n=>n.authz!=null||(n.onPublish?.length??0)>0||(n.onDisconnect?.length??0)>0||n.notify!=null)}async function M(e,n,t){let r=j.dirname(n),o;if(ee(e)){let i=await Re(n);o={name:Le,content:i}}let s=[];for(let[i,a]of t)for(let c of a){let d=await te(c.path,r);s.push({key:i,handle:c.handle,name:Ne(i,c.handle),content:d})}return Me(s),{hookScript:o,extensionScripts:s}}function Me(e){let n=new Map;for(let t of e){let r=n.get(t.name);if(r)throw new g(`Extensions "${r.handle}" (channel "${r.key}") and "${t.handle}" (channel "${t.key}") both map to the upload name "${t.name}". Rename a handle or channel so they differ by more than punctuation.`);n.set(t.name,t)}}function R(e,n){for(let t of n){let r=e.channels[t.key]?.extensions[t.handle];r&&(r.script=t.name)}}async function Re(e){let n=await Ae(j.join(Te(),"portal-cli-hooks-")),t=j.join(n,"hooks.mjs");try{return await Ue(t,Z(e),"utf8"),await te(t,j.dirname(e))}finally{await Oe(n,{recursive:!0,force:!0})}}async function te(e,n){return(await De.build({entryPoints:[e],bundle:!0,write:!1,format:"esm",target:"es2022",platform:"neutral",logLevel:"silent",absWorkingDir:n})).outputFiles[0].text}function Ne(e,n){return`ext-${Q(e)}-${Q(n)}`}function Q(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}import{existsSync as qe}from"fs";import Ke from"path";var b=class extends Error{};async function N(e,n){let t=new Map;for(let[r,o]of Object.entries(e.channels??{})){let s=o.extensions;if(!s||Object.keys(s).length===0)continue;let i=[];for(let[a,c]of Object.entries(s)){let d=Ke.resolve(n,c);if(!qe(d))throw new b(`Channel "${r}" attaches extension "${a}" from "${c}", but that file does not exist.`);let l=await $(d),h=He(l.default);if(!h)throw new b(`Extension "${a}" (${c}) must \`export default defineExtension(...)\` with a static manifest.`);i.push({handle:a,source:c,namespace:h.namespace,transport:h.transport,path:d})}t.set(r,i)}return t}function He(e){if(typeof e!="function"&&typeof e!="object")return null;let n=e?.manifest;if(n==null||typeof n!="object")return null;let{namespace:t,transport:r}=n;return typeof t!="string"||!t.endsWith(".")||r!=="ws"&&r!=="http"?null:{namespace:t,transport:r}}async function ne(e){let n=G(e.cwd,e.configPath),t=await X(n),r=_e.dirname(n),o=await N(t,r);L(t,o);let s=D(t,o),i=await M(t,n,o);R(s,i.extensionScripts);let a=new x(e.apiUrl,e.secret),{versionId:c,alreadyDeployed:d}=await a.createDeploy({extract:s,hookScript:i.hookScript,extensionScripts:i.extensionScripts.length>0?i.extensionScripts:void 0});return await a.activate(c),{configPath:n,versionId:c,alreadyDeployed:d,channels:Object.keys(s.channels),hasHookScript:!!i.hookScript,extensionCount:i.extensionScripts.length}}async function re(e){await new x(e.apiUrl,e.secret).setSecret(e.name,e.value)}import oe from"path";var ze="https://api.useportal.co";function ie(e,n){let t=oe.relative(n,e);return t&&!t.startsWith("..")&&!oe.isAbsolute(t)?t:e}function y(){return(process.env.PORTAL_API_URL||ze).replace(/\/+$/,"")}function q(){let e=process.env.PORTAL_SECRET;if(!e)throw new Error("Set PORTAL_SECRET to your project's secret key (sk_...) before running this command.");return e}async function se(e){if(e!=null&&e!=="")return e;if(process.stdin.isTTY){let t=await Xe("Value: ");if(!t)throw new Error("No value entered.");return t}let n=await We();if(!n)throw new Error("No value provided. Pass --value <value> or pipe the value on stdin.");return n}async function We(){let e=[];for await(let n of process.stdin)e.push(n);return Buffer.concat(e).toString("utf8").trim()}var Fe=10,Be=13,Je=4,Ye=3,Ve=8,Ge=127;function Xe(e){return new Promise(n=>{let t=process.stdin;process.stdout.write(e),t.setRawMode?.(!0),t.resume(),t.setEncoding("utf8");let r="",o=s=>{for(let i of s){let a=i.charCodeAt(0);if(a===Fe||a===Be||a===Je){t.setRawMode?.(!1),t.pause(),t.removeListener("data",o),process.stdout.write(`
54
+ `),n(r);return}a===Ye&&(t.setRawMode?.(!1),t.pause(),t.removeListener("data",o),process.stdout.write(`
55
+ `),process.exit(1)),a===Ve||a===Ge?r=r.slice(0,-1):r+=i}};t.on("data",o)})}async function ae(e){let n=await S();return new f(e.apiUrl).createApiKey(n,e.environmentId,e.keyType)}import{Portal as Qe}from"@portalsdk/core";import{WebSocket as Ze}from"ws";typeof globalThis.WebSocket>"u"&&(globalThis.WebSocket=Ze);function et(e){let n=new Date(e.timestamp).toISOString(),t=e.sender.anon?`anon:${e.sender.id}`:e.sender.username??e.sender.id,r=typeof e.content=="string"?e.content:JSON.stringify(e.content);return`[${n}] ${t}: ${r}`}function ce(e){let n=e.log??(s=>console.log(s)),t=e.info??(s=>console.error(s)),o=new Qe({apiKey:e.apiKey,...e.apiUrl?{apiUrl:e.apiUrl}:{},...e.realtimeUrl?{realtimeUrl:e.realtimeUrl}:{}}).channel(e.channelId,{history:"none"});return new Promise(s=>{let i=!1,a=()=>{i||(i=!0,process.off("SIGINT",a),c(),d(),o.release(),s())},c=o.on("message",l=>{n(et(l))}),d=o.on("status",(l,h)=>{l==="ready"&&t(`Listening on ${e.channelId} (Ctrl-C to exit)\u2026`),l==="blocked"&&(t(`Connection blocked${h?`: ${h.message}`:""}.`),a())});process.on("SIGINT",a),o.acquire()})}async function le(e){let n=await S(),t=new f(e.apiUrl),r=await t.fetchIdentity(n),{organizations:o}=r;if(o.length===0)throw new Error("Your account has no organization yet \u2014 create one in the dashboard first.");if(o.length>1){let s=o.map(i=>i.name).join(", ");throw new Error(`Your account belongs to multiple organizations (${s}). portal projects create only supports a single organization right now.`)}return t.createProject(n,o[0].id,e.name)}var rt=tt(import.meta.url),ot=rt("../package.json").version,m=new nt;m.name("portal").description("Sign in, deploy your Portal configuration, and manage secrets.").addHelpText("after",`
56
+ Authentication:
57
+ login signs you in as a user (for account-level commands)
58
+ PORTAL_SECRET your project's secret key (sk_...), used by deploy and secrets
59
+ `).version(ot);m.command("login").description("Sign in to Portal.").option("--name <name>","Label for this machine (default: its hostname)").action(async e=>{await w(async()=>{let n=await F({apiUrl:y(),name:e.name}),t=n.identity?ue(n.identity):null;console.log(`${p.green("\u2713")} Signed in${t?` as ${p.bold(t)}`:""}.`)})});m.command("whoami").description("Show who you are signed in as.").action(async()=>{await w(async()=>{let e=await B({apiUrl:y()});if(!e.loggedIn){console.log("Not signed in. Run: portal login");return}console.log(ue(e.identity));let{organizations:n}=e.identity;n.length>0&&console.log(p.dim(` ${n.map(t=>t.name).join(", ")}`))})});m.command("logout").description("Sign out of Portal on this machine.").action(async()=>{await w(async()=>{if(!(await J({apiUrl:y()})).wasLoggedIn){console.log("You were not signed in.");return}console.log(`${p.green("\u2713")} Signed out.`)})});m.command("deploy").description("Deploy your portal.config.ts.").option("-c, --config <path>","Path to your config file (default: portal.config.ts)").action(async e=>{await w(async()=>{let n=q(),t=await ne({cwd:process.cwd(),configPath:e.config,apiUrl:y(),secret:n});ct(t)})});var it=m.command("secrets").description("Manage your project secrets.");it.command("set").argument("<name>",'Secret name, referenced in your config with env("NAME")').description("Set a project secret.").option("--value <value>","The secret value (otherwise read from stdin)").action(async(e,n)=>{await w(async()=>{let t=q(),r=await se(n.value);await re({name:e,value:r,apiUrl:y(),secret:t}),console.log(`${p.green("\u2713")} Secret ${p.bold(e)} set.`)})});var st=m.command("projects").description("Manage your Portal projects.");st.command("create").argument("<name>","Project name").description("Create a project (auto-picks your organization) and its two environments.").action(async e=>{await w(async()=>{let n=await le({apiUrl:y(),name:e});console.log(`${p.green("\u2713")} Created project ${p.bold(n.project.name)}`),console.log(` Project: ${n.project.id}`);for(let t of n.environments)console.log(` ${lt(t.type)}: ${t.id}`)})});var at=m.command("keys").description("Manage environment API keys.");at.command("create").description("Create an API key for an environment. The value is printed once.").requiredOption("--env <envId>","Environment id").requiredOption("--type <type>","public|secret").action(async e=>{await w(async()=>{if(e.type!=="public"&&e.type!=="secret")throw new Error("--type must be public or secret.");let n=await ae({apiUrl:y(),environmentId:e.env,keyType:e.type});console.log(`${p.green("\u2713")} Created ${n.keyType} key.`),console.log(` ${n.value}`),console.log(p.dim(" This value is shown once \u2014 store it now."))})});m.command("listen").argument("<channelId>","Channel id to listen on").description("Connect anonymously and print every incoming message as one line.").requiredOption("--key <key>","Publishable key (pk_...)").action(async(e,n)=>{await w(async()=>{await ce({apiKey:n.key,channelId:e})})});m.parseAsync(process.argv);async function w(e){try{await e()}catch(n){console.error(`${p.red("Error:")} ${n instanceof Error?n.message:String(n)}`),process.exitCode=1}}function ct(e){e.alreadyDeployed?console.log(`${p.green("\u2713")} Already up to date \u2014 version ${p.bold(e.versionId)}`):(console.log(`${p.green("\u2713")} Deployed ${p.bold(ie(e.configPath,process.cwd()))}`),console.log(` Version ${e.versionId}`));let{channels:n}=e;console.log(` ${n.length} channel ${pe(n.length,"override","overrides")}: ${n.join(", ")}`);let t=[];e.hasHookScript&&t.push("hooks"),e.extensionCount>0&&t.push(`${e.extensionCount} ${pe(e.extensionCount,"extension","extensions")}`),t.length>0&&console.log(` Uploaded: ${t.join(", ")}`),console.log(p.dim(" Channels with active connections keep their current configuration until they restart; new connections use this version right away."))}function pe(e,n,t){return e===1?n:t}function lt(e){return e.charAt(0).toUpperCase()+e.slice(1)}function ue(e){let{firstName:n,lastName:t,email:r}=e.user,o=[n,t].filter(Boolean).join(" ").trim();return o?`${o} (${r})`:r}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portalsdk/cli",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "The Portal command line: deploy your configuration and manage secrets.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,12 +23,15 @@
23
23
  "realtime"
24
24
  ],
25
25
  "dependencies": {
26
+ "@portalsdk/core": "^0.1.3",
26
27
  "commander": "^14.0.2",
27
28
  "esbuild": "^0.25.0",
28
- "picocolors": "^1.1.1"
29
+ "picocolors": "^1.1.1",
30
+ "ws": "^8.18.0"
29
31
  },
30
32
  "devDependencies": {
31
33
  "@types/node": "^24",
34
+ "@types/ws": "^8.5.13",
32
35
  "tsup": "^8.5.0",
33
36
  "tsx": "^4.7.1",
34
37
  "typescript": "^5.8.3",