@hyperscale0/cli 1.0.0-alpha.3 → 1.0.0-alpha.4
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/README.md +12 -4
- package/hyperscale.js +46 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
# @hyperscale0/cli
|
|
2
2
|
|
|
3
|
-
The Hyperscale command line. It
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The Hyperscale founder command line. It logs a founder in, discovers public
|
|
4
|
+
Blueprints, composes Products, manages sandbox API keys, and calls one
|
|
5
|
+
Product's current operation catalog.
|
|
6
6
|
|
|
7
7
|
## Use
|
|
8
8
|
|
|
9
9
|
Request access at https://hyperscale0.ai/request-access.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
|
|
12
|
+
bunx @hyperscale0/cli auth login --email founder@example.com
|
|
13
|
+
bunx @hyperscale0/cli blueprint list
|
|
14
|
+
bunx @hyperscale0/cli product create --blueprint vehicle-escrow-marketplace --name "Vehicle Escrow"
|
|
15
|
+
bunx @hyperscale0/cli key create --name "Founder laptop"
|
|
13
16
|
|
|
17
|
+
export HYPERSCALE_API_KEY=<your Product API key>
|
|
14
18
|
bunx @hyperscale0/cli whoami
|
|
15
19
|
bunx @hyperscale0/cli ops list account
|
|
16
20
|
bunx @hyperscale0/cli call account.list --input '{"limit":5}'
|
|
@@ -21,5 +25,9 @@ machine-readable output.
|
|
|
21
25
|
|
|
22
26
|
Point it at another host with `--base-url`, and at the live plane with
|
|
23
27
|
`--environment live`. Live keys and sandbox keys are never interchangeable.
|
|
28
|
+
Password, MFA code, and user-session bearer never use arguments or environment
|
|
29
|
+
variables. The CLI stores the session in macOS Keychain, with a 0600 file
|
|
30
|
+
fallback on systems where Keychain is unavailable. Passkey login stays in the
|
|
31
|
+
portal at https://hyperscale0.ai/signin.
|
|
24
32
|
|
|
25
33
|
Run `bunx @hyperscale0/cli --help` for the full option list.
|
package/hyperscale.js
CHANGED
|
@@ -1,32 +1,67 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const
|
|
2
|
+
import{spawn as ce}from"node:child_process";import{createHash as ue}from"node:crypto";import{chmod as x,mkdir as de,readdir as le,readFile as L,rename as pe,unlink as fe,writeFile as we}from"node:fs/promises";import{homedir as me}from"node:os";import{basename as he,dirname as R,join as P}from"node:path";const v="https://hyperscale0.ai",O="sandbox",_="hyperscale-cli-session",W=process.platform==="darwin"?"/usr/bin/security":null,ye="config.json",z=3e4,ke=`hyperscale <command> [options]
|
|
3
3
|
|
|
4
4
|
Commands:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
auth login Log in with email, password, and MFA when required
|
|
6
|
+
auth status Show the active nonsecret profile
|
|
7
|
+
auth logout Revoke and remove the active user session
|
|
8
|
+
blueprint list List public Blueprints without authentication
|
|
9
|
+
blueprint show <id> Show one public Blueprint
|
|
10
|
+
product create Create a Product from a Blueprint
|
|
11
|
+
product list List Products in the signed-in company
|
|
12
|
+
product show [id] Show a Product (default: selected Product)
|
|
13
|
+
key create Create a sandbox Product API key
|
|
14
|
+
key list List keys for the selected Product
|
|
15
|
+
key revoke <id> Revoke a Product API key
|
|
16
|
+
whoami Show the product this API key resolves to
|
|
17
|
+
ops list [filter] List the product's callable operations
|
|
18
|
+
call <operation> Call one operation by name or operation id
|
|
8
19
|
|
|
9
20
|
Options:
|
|
10
21
|
--api-key <key> Product API key (default: $HYPERSCALE_API_KEY)
|
|
11
|
-
--base-url <url> API origin (default: $HYPERSCALE_BASE_URL, else ${
|
|
12
|
-
--environment <name> sandbox or live (default: $HYPERSCALE_ENVIRONMENT, else ${
|
|
22
|
+
--base-url <url> API origin (default: $HYPERSCALE_BASE_URL, else ${v})
|
|
23
|
+
--environment <name> sandbox or live (default: $HYPERSCALE_ENVIRONMENT, else ${O})
|
|
24
|
+
--email <email> Login email (password and MFA stay on hidden stdin)
|
|
25
|
+
--blueprint <id> Blueprint for product create
|
|
26
|
+
--name <name> Product or key name
|
|
27
|
+
--product <id> Product override for key commands
|
|
13
28
|
--input <json> JSON input object for call
|
|
29
|
+
--input-file <path> Read call input from a JSON file
|
|
14
30
|
--idempotency-key <key> Idempotency key for a mutation (default: generated)
|
|
31
|
+
--passkey Print the portal pointer for passkey login
|
|
15
32
|
--json Print machine-readable JSON
|
|
16
33
|
--version Print the CLI version
|
|
17
34
|
--help Print this help
|
|
18
35
|
|
|
19
|
-
|
|
20
|
-
|
|
36
|
+
Password, MFA code, and user-session bearer never ride in arguments or
|
|
37
|
+
environment variables. Passkey login runs in the portal at ${v}/signin.
|
|
38
|
+
|
|
39
|
+
The Product key rides in the environment, never on the command line, unless
|
|
40
|
+
you are pasting a one-off into a throwaway shell.
|
|
21
41
|
|
|
22
42
|
Request access at https://hyperscale0.ai/request-access.
|
|
23
43
|
|
|
24
44
|
Examples:
|
|
25
45
|
hyperscale whoami
|
|
46
|
+
hyperscale auth login --email founder@example.com
|
|
47
|
+
hyperscale blueprint list
|
|
48
|
+
hyperscale product create --blueprint vehicle-escrow-marketplace --name "Vehicle Escrow"
|
|
49
|
+
hyperscale key create --name "Founder laptop"
|
|
26
50
|
hyperscale ops list account
|
|
27
51
|
hyperscale call account.list --input '{"limit":5}'
|
|
52
|
+
hyperscale call account.list --input-file input.json
|
|
53
|
+
printf '{"limit":5}' | hyperscale call account.list
|
|
28
54
|
hyperscale call account.retrieve --input '{"accountId":"acct_sandbox_..."}'
|
|
29
|
-
`;class
|
|
30
|
-
|
|
31
|
-
`)}
|
|
55
|
+
`;class c extends Error{}const ge=new Set(["--api-key","--base-url","--blueprint","--email","--environment","--idempotency-key","--input","--input-file","--name","--product"]);function $e(e){const n=[],t={};for(let s=0;s<e.length;s+=1){const o=e[s];if(!o.startsWith("--")){n.push(o);continue}if(ge.has(o)){const r=e[s+1];if(r===void 0)throw new c(`${o} needs a value`);t[o]=r,s+=1;continue}if(o==="--help"||o==="--version"||o==="--json"||o==="--passkey"){t[o]=!0;continue}throw new c(`Unknown option: ${o}`)}return{flags:t,positional:n}}function be(e,n){const t=e["--api-key"]??n.HYPERSCALE_API_KEY;if(!t)throw new c("No API key. Set HYPERSCALE_API_KEY, or pass --api-key. Request access at https://hyperscale0.ai/request-access.");const s=e["--environment"]??n.HYPERSCALE_ENVIRONMENT??O;if(s!=="sandbox"&&s!=="live")throw new c(`Unknown environment: ${s}. Use sandbox or live.`);const o=(e["--base-url"]??n.HYPERSCALE_BASE_URL??v).replace(/\/+$/,"");return{apiKey:t,baseUrl:o,environment:s}}function j(e,n,t={}){const s=e["--environment"]??n.HYPERSCALE_ENVIRONMENT??t.environment??O;if(s!=="sandbox"&&s!=="live")throw new c(`Unknown environment: ${s}. Use sandbox or live.`);const o=(e["--base-url"]??n.HYPERSCALE_BASE_URL??t.baseUrl??v).replace(/\/+$/,"");let r;try{r=new URL(o)}catch{r=null}const i=r?.origin??null;if(!i||i!==o)throw new c(`Invalid API origin: ${o}. Use an origin such as https://hyperscale0.ai.`);const a=["localhost","127.0.0.1","[::1]","::1"].includes(r.hostname);if(r.protocol!=="https:"&&!(r.protocol==="http:"&&a))throw new c(`Refusing cleartext API origin ${o}. Use HTTPS unless the API runs on loopback.`);return{baseUrl:o,environment:s}}function Q(e){return e.HYPERSCALE_CONFIG_DIR??P(me(),".config","hyperscale")}function X(e){return P(Q(e),ye)}function g(e,n){return P(Q(n),`session-${b(e)}.json`)}async function J(e,n){const t=g(e,n),s=`${he(t)}.`;let o;try{o=await le(R(t),{withFileTypes:!0})}catch(r){if(r&&r.code==="ENOENT")return;throw r}for(const r of o){if(!r.isFile()||!r.name.startsWith(s))continue;const i=r.name.slice(s.length);/^\d+\.[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.tmp$/.test(i)&&await A(P(R(t),r.name))}}async function Se(e){try{return JSON.parse(await L(e,"utf8"))}catch(n){if(n&&n.code==="ENOENT")return null;throw n instanceof SyntaxError?new c(`${e} is not valid JSON`):n}}async function B(e,n){const t=Ie(e);await de(t,{mode:448,recursive:!0}),await x(t,448);const s=`${e}.${process.pid}.${globalThis.crypto.randomUUID()}.tmp`;await we(s,n,{flag:"wx",mode:384}),await x(s,384);try{await pe(s,e),await x(e,384)}catch(o){throw await A(s),o}}function Ie(e){return R(e)}async function A(e){try{await fe(e)}catch(n){if(!n||n.code!=="ENOENT")throw n}}function Pe(e,n){if(!e||typeof e!="object"||Array.isArray(e))throw new c(`${n} does not contain a CLI profile`);const{baseUrl:t,email:s,environment:o,selectedProduct:r}=e;if(typeof t!="string"||typeof s!="string"||o!=="sandbox"&&o!=="live"||!(r===null||typeof r=="string"))throw new c(`${n} does not contain a valid CLI profile`);return{baseUrl:t,email:s,environment:o,selectedProduct:r}}async function E(e){const n=X(e),t=await Se(n);return t===null?null:Pe(t,n)}async function Z(e,n){const t={email:n.email,baseUrl:n.baseUrl,environment:n.environment,selectedProduct:n.selectedProduct??null};await B(X(e),`${JSON.stringify(t,null,2)}
|
|
56
|
+
`)}function b(e){return ue("sha256").update(`${e.baseUrl}\0${e.environment}\0${e.email}`).digest("hex")}async function F(e,n){return W===null?{error:null,status:null,stdout:"",unavailable:!0}:await new Promise(t=>{const s=ce(W,e,{stdio:["pipe","pipe","pipe"]}),o=[];s.stdout.on("data",r=>o.push(r)),s.stderr.on("data",()=>{}),s.once("error",r=>t({error:r,status:null,stdout:"",unavailable:r?.code==="ENOENT"})),s.once("close",r=>t({error:null,status:r,stdout:Buffer.concat(o).toString("utf8"),unavailable:!1})),n===void 0?s.stdin.end():s.stdin.end(`${n}
|
|
57
|
+
`)})}function ee(e){try{const n=typeof e=="string"?JSON.parse(e):e;if(n&&typeof n=="object"&&!Array.isArray(n)&&typeof n.sessionToken=="string"&&typeof n.tenantId=="string")return{sessionToken:n.sessionToken,tenantId:n.tenantId}}catch{}return null}async function ve(e,n,t){await J(e,t);const s=b(e),o=JSON.stringify(n),r=["add-generic-password","-a",s,"-s",_,"-U","-X",Buffer.from(o,"utf8").toString("hex")].join(" ");return(await F(["-i"],r)).status===0?(await A(g(e,t)),"keychain"):(await B(g(e,t),`${JSON.stringify({account:s,credential:n})}
|
|
58
|
+
`),"file")}async function Ae(e,n){const t=b(e);try{const s=JSON.parse(await L(g(e,n),"utf8"));if(!s||typeof s!="object"||Array.isArray(s)||s.account!==t)return{state:"invalid"};const o=ee(s.credential);return o?{credential:o,state:"valid"}:{state:"invalid"}}catch(s){return s&&s.code==="ENOENT"?{state:"missing"}:{state:"invalid"}}}async function M(e,n){await J(e,n);const t=await Ae(e,n);if(t.state==="valid")return t.credential;if(t.state==="invalid")return null;const s=await F(["find-generic-password","-a",b(e),"-s",_,"-w"]);if(s.status===0){const o=ee(s.stdout.trim());if(o)return o}return null}async function H(e,n){await J(e,n);const t=b(e),s=await F(["delete-generic-password","-a",b(e),"-s",_]);return!s.unavailable&&s.status!==0&&s.status!==44?(await B(g(e,n),`${JSON.stringify({account:t,revoked:!0})}
|
|
59
|
+
`),{keychainDeleted:!1}):(await A(g(e,n)),{keychainDeleted:!0})}async function S(e,n){const t=await E(n);if(!t)throw new c('Not logged in. Run "hyperscale auth login".');const s=j(e,n,t);if(s.baseUrl!==t.baseUrl||s.environment!==t.environment)throw new c(`The active session belongs to ${t.baseUrl} (${t.environment}). Log in again to change it.`);const o=await M(t,n);if(!o)throw new c('Session credential is missing. Run "hyperscale auth login".');return{...t,...o}}function te(e){return e.replace(/\p{Cc}/gu,n=>`
|
|
60
|
+
\r`.includes(n)?n:"")}function T(e){try{return new URL(e).origin}catch{return null}}function ne(e,n){let t=e;for(const s of n)typeof s=="string"&&s.length>0&&(t=t.replaceAll(s,"[redacted]"));return t}async function oe(e,n,t={},s){const o=`${e.baseUrl}${n}`,r=T(e.baseUrl);if(T(o)!==r)throw new c(`The requested path ${n} leaves the ${r} origin; refusing the request.`);try{const i=await fetch(o,{...t,headers:{accept:"application/json",...s?{authorization:`Bearer ${s}`}:{},"x-hyperscale-environment":e.environment,...t.headers},signal:AbortSignal.timeout(z),redirect:"manual"});if(i.status>=300&&i.status<400)throw new c(`${o} redirected, so no credential was sent on. Point --base-url at the origin the API answers on.`);return{response:i,text:te(await i.text())}}catch(i){throw i instanceof c?i:new c(`Could not reach ${o}: ${i.message}`)}}async function y(e,n,t={},s,o=[]){const{response:r,text:i}=await oe(e,n,t,s);if(!r.ok)throw C(r,i,o);try{return JSON.parse(i)}catch{throw new c(`HTTP ${r.status} returned invalid JSON`)}}function I(e){return{body:JSON.stringify(e),headers:{"content-type":"application/json","idempotency-key":globalThis.crypto.randomUUID()},method:"POST"}}let m="";async function K(e,n=!1){return process.stderr.write(e),process.stdin.isTTY&&typeof process.stdin.setRawMode=="function"?await Te(n):await Ee()}async function Ee(){const e=se();if(e!==null)return e;if(process.stdin.readableEnded){const n=m.replace(/\r$/,"");return m="",n}return await new Promise((n,t)=>{const s=a=>{m+=a.toString("utf8");const d=se();d!==null&&(i(),n(d))},o=()=>{const a=m.replace(/\r$/,"");m="",i(),n(a)},r=a=>{i(),t(a)},i=()=>{process.stdin.off("data",s),process.stdin.off("end",o),process.stdin.off("error",r),process.stdin.pause()};process.stdin.on("data",s),process.stdin.once("end",o),process.stdin.once("error",r),process.stdin.resume()})}function se(){const e=/\r?\n/.exec(m);if(!e||e.index===void 0)return null;const n=m.slice(0,e.index);return m=m.slice(e.index+e[0].length),n}async function Te(e){return await new Promise((n,t)=>{const s=process.stdin.isRaw;let o="";const r=(d,l)=>{process.stdin.off("data",a),process.stdin.off("error",i),process.stdin.setRawMode(!!s),process.stdin.pause(),process.stderr.write(`
|
|
61
|
+
`),l?t(l):n(d)},i=d=>r("",d),a=d=>{for(const l of d.toString("utf8")){if(l===""){r("",new c("Login cancelled"));return}if(l==="\r"||l===`
|
|
62
|
+
`){r(o);return}if(l==="\x7F"||l==="\b"){if(o.length===0)continue;o=o.slice(0,-1),e||process.stderr.write("\b \b");continue}o+=l,e||process.stderr.write(l)}};process.stdin.setRawMode(!0),process.stdin.on("data",a),process.stdin.once("error",i),process.stdin.resume()})}async function Ce(){const e=[];for await(const n of process.stdin)e.push(Buffer.from(n));return Buffer.concat(e).toString("utf8")}async function re(e,n,t={}){const s=`${e.baseUrl}${n}`,o=T(e.baseUrl);if(T(s)!==o)throw new c(`The product document puts ${n} on another origin than the ${o} you configured; refusing to send the key there.`);try{const r=await fetch(s,{...t,headers:{accept:"application/json",authorization:`Bearer ${e.apiKey}`,"x-hyperscale-environment":e.environment,...t.headers},signal:AbortSignal.timeout(z),redirect:"manual"});if(r.status>=300&&r.status<400)throw new c(`${s} redirected, so the key was not sent on. Point --base-url at the origin the API answers on; the usual cause is http:// where it serves https://.`);return{response:r,text:te(await r.text())}}catch(r){throw r instanceof c?r:new c(`Could not reach ${s}: ${r.message}`)}}async function Y(e){const{response:n,text:t}=await re(e,"/v1/llms.txt");if(!n.ok)throw C(n,t,[e.apiKey]);const s=/^# (.+)$/m.exec(t),o=/^> Generated for composition (\S+) /m.exec(t),r=/^## Operations \((\d+)\)$/m.exec(t);if(!s||!r)throw new Error("The product document is not in the expected format; upgrade the CLI.");const i=[],a=/^- ([A-Z]+) (\S+) · (\S+) \((\S+); idempotency (\S+)\)$/gm;for(const f of t.matchAll(a))i.push({idempotency:f[5],method:f[1],name:f[3],operationId:f[4],path:f[2]});const d=Number(r[1]);if(i.length!==d)throw new Error(`The product document declares ${d} operations but ${i.length} parsed; upgrade the CLI.`);const l=/^- Package: (.+)$/m.exec(t);return{compositionId:o?o[1]:null,operations:i,packageName:l?l[1].trim():null,title:s[1].trim()}}function C(e,n,t=[]){let s=ne(n.trim(),t);try{const o=JSON.parse(n);o&&o.error&&typeof o.error.message=="string"&&(s=ne(`${o.error.code}: ${o.error.message}`,t))}catch{}return new c(`HTTP ${e.status}${s?` \xB7 ${s}`:""}`)}function ie(e,n){return{authenticated:n,baseUrl:e.baseUrl,email:e.email,environment:e.environment,selectedProduct:e.selectedProduct}}function Ne(e,n){if(!e||typeof e!="object"||e.status!=="authenticated"||typeof e.sessionToken!="string"||typeof e.tenantId!="string")throw new c(`Login did not return a new session. Use the portal at ${n}/signin.`);return e}async function Ue(e,n){const t=j(e,n);if(e["--passkey"]){u(`Passkey login runs in the portal: ${t.baseUrl}/signin`);return}const s=e["--email"]??(await K("Email: ",!1)).trim();if(!s)throw new c("Login email is required");const o=await K("Password: ",!0);if(!o)throw new c("Password is required");let r=await y(t,"/v1/identity/login",I({email:s,password:o}),void 0,[o]);if(r?.status==="mfa_required"){const w=r.mfaChallengeToken;if(typeof w!="string"||w.length===0)throw new c(`MFA must continue in the portal: ${t.baseUrl}/signin`);for(;r?.status!=="authenticated";){const h=await K("MFA code: ",!0);if(!h)throw new c("MFA code is required");if(r=await y(t,"/v1/identity/login/mfa/verify",I({code:h,mfaChallengeToken:w}),void 0,[o,w,h]),r?.status==="denied"){const k=r.attemptsRemaining;if(typeof k!="number"||k<=0)throw new c(`MFA was denied. Continue in the portal: ${t.baseUrl}/signin`);process.stderr.write(`MFA denied. ${k} attempts remain.
|
|
63
|
+
`)}}}const i=Ne(r,t.baseUrl),a=await E(n),d={email:s,...t,selectedProduct:a&&a.email===s&&a.baseUrl===t.baseUrl&&a.environment===t.environment?a.selectedProduct:null},l=await ve(d,{sessionToken:i.sessionToken,tenantId:i.tenantId},n);try{await Z(n,d)}catch(w){throw await H(d,n),w}l==="file"&&process.stderr.write(`macOS Keychain is unavailable. The session is in ${g(d,n)} with mode 0600.
|
|
64
|
+
`);const f=ie(d,!0);e["--json"]?p(f):u(`Logged in as ${s} to ${t.baseUrl} (${t.environment}).`)}async function xe(e,n){const t=await E(n);if(!t){const r={authenticated:!1};e["--json"]?p(r):u("Not logged in.");return}const s=await M(t,n),o=ie(t,s!==null);if(e["--json"]){p(o);return}u(s?`Logged in as ${t.email}.`:"Session credential missing."),u(`API: ${t.baseUrl}`),u(`Environment: ${t.environment}`),u(`Product: ${t.selectedProduct??"not selected"}`)}async function Le(e,n){const t=await E(n);if(!t){e["--json"]?p({status:"signed_out"}):u("Already logged out.");return}const s=await M(t,n);if(!s){const d=await H(t,n);q(e,{cleanup:d,message:"Already logged out.",status:"signed_out"});return}const{response:o,text:r}=await oe(t,"/v1/identity/logout",I({}),s.sessionToken);if(!o.ok&&o.status!==401&&o.status!==403)throw C(o,r,[s.sessionToken]);const i=await H(t,n);if(o.status===401||o.status===403){q(e,{cleanup:i,message:"The server had already rejected this session.",serverStatus:o.status,status:"signed_out"});return}let a={};try{a=JSON.parse(r)}catch{}q(e,{cleanup:i,message:"The server revoked the session.",revokedAt:a.revokedAt??null,status:a.status??"revoked"})}function q(e,n){const t=n.cleanup.keychainDeleted?"complete":"tombstoned";e["--json"]?p({credentialCleanup:t,...n.revokedAt!==void 0?{revokedAt:n.revokedAt}:{},...n.serverStatus!==void 0?{serverStatus:n.serverStatus}:{},status:n.status}):u(n.message),n.cleanup.keychainDeleted||process.stderr.write(`Keychain cleanup failed. A 0600 profile tombstone blocks the stale session.
|
|
65
|
+
`)}function N(e,n){if(!e||typeof e!="object"||!Array.isArray(e.items))throw new c(`${n} returned an invalid collection`);return e.items}async function D(e,n,t,s,o,r=[]){const i=[],a=new Set;let d,l;for(;;){const k=new URLSearchParams(t);d&&k.set("cursor",d);const ae=k.size>0?`?${k}`:"",U=await y(e,`${n}${ae}`,{},o,r);l===void 0&&(l=U),i.push(...N(U,s));const $=U.nextCursor;if(typeof $!="string"||$.length===0)break;if(a.has($))throw new c(`${s} repeated cursor ${$}`);a.add($),d=$}const{items:f,nextCursor:w,...h}=l;return{...h,items:i}}async function Re(e,n){const t=await D(e,"/v1/blueprints",{},"blueprint.list"),s=N(t,"blueprint.list");if(n["--json"]){p(t);return}for(const o of s)u(`${o.id} ${o.name}${o.summary?` ${o.summary}`:""}`)}async function Oe(e,n,t){if(!t)throw new c("blueprint show needs a Blueprint id");const s=await y(e,`/v1/blueprints/${encodeURIComponent(t)}`);p(s)}async function _e(e,n){const t=e["--blueprint"],s=e["--name"];if(!t)throw new c("product create needs --blueprint <id>");if(!s)throw new c("product create needs --name <name>");const o=await S(e,n),r=await y(o,"/v1/workspaces/blueprint-compose",I({blueprintId:t,name:s,tenantId:o.tenantId}),o.sessionToken,[o.sessionToken]);if(typeof r?.workspaceId!="string"||typeof r?.blueprintId!="string"||typeof r?.workspaceBuildId!="string")throw new c("workspace.blueprint.compose returned an invalid Product");await Z(n,{...o,selectedProduct:r.workspaceId});const i={blueprintId:r.blueprintId,nextSteps:[`hyperscale product show ${r.workspaceId}`,'hyperscale key create --name "Founder CLI"'],workspaceBuildId:r.workspaceBuildId,workspaceId:r.workspaceId};if(e["--json"]){p(i);return}u(`Product: ${i.workspaceId}`),u(`Blueprint: ${i.blueprintId}`),u(`Build: ${i.workspaceBuildId}`),u("Next:");for(const a of i.nextSteps)u(` ${a}`)}function je(e){return new URLSearchParams({tenantId:e}).toString()}async function Je(e,n){const t=await S(e,n),s=await D(t,"/v1/products",{tenantId:t.tenantId},"product.list",t.sessionToken,[t.sessionToken]),o=N(s,"product.list");if(e["--json"]){p(s);return}for(const r of o){const i=r.workspaceId??r.id??"unknown",a=r.displayName??r.name??"unnamed";u(`${i} ${a}${r.status?` ${r.status}`:""}`)}}async function Be(e,n,t){const s=await S(e,n),o=t??s.selectedProduct;if(!o)throw new c("product show needs an id because no Product is selected. Create a Product first.");const r=await y(s,`/v1/products/${encodeURIComponent(o)}?${je(s.tenantId)}`,{},s.sessionToken,[s.sessionToken]);p(r)}function V(e,n){const t=n["--product"]??e.selectedProduct;if(!t)throw new c("No Product selected. Pass --product <id> or create a Product first.");return t}async function Fe(e,n){const t=await S(e,n),s=V(t,e),o=await y(t,"/v1/sandbox-api-keys",I({name:e["--name"]??"Founder CLI",tenantId:t.tenantId,workspaceId:s}),t.sessionToken,[t.sessionToken]);if(e["--json"]){p(o);return}u(`Key: ${o.apiKeyId??"unknown"}`),u(`Product: ${o.workspaceId??s}`),u(`Status: ${o.status??"unknown"}`),o.secret?(u(`Secret: ${o.secret}`),u("Store it now. Hyperscale will not return it again.")):u("The key already existed, so the one-time secret is unavailable.")}async function Me(e,n){const t=await S(e,n),s=V(t,e),o=await D(t,"/v1/api-keys",{tenantId:t.tenantId},"api_key.list",t.sessionToken,[t.sessionToken]),r=N(o,"api_key.list").filter(a=>a.workspaceId===s),i={...o,items:r};if(e["--json"]){p(i);return}for(const a of r)u(`${a.apiKeyId} ${a.name??"unnamed"} ${a.status??"unknown"}`)}async function He(e,n,t){if(!t)throw new c("key revoke needs an API key id");const s=await S(e,n),o=V(s,e),r=await y(s,`/v1/api-keys/${encodeURIComponent(t)}/revoke`,I({tenantId:s.tenantId,workspaceId:o}),s.sessionToken,[s.sessionToken]);e["--json"]?p(r):u(`Key ${r.apiKeyId??t}: ${r.status??"revoked"}`)}async function Ke(e,n){const t=await Y(e);if(n["--json"]){u(JSON.stringify({baseUrl:e.baseUrl,compositionId:t.compositionId,environment:e.environment,operationCount:t.operations.length,packageName:t.packageName,product:t.title},null,2));return}const s=[["Product",t.title],["Composition",t.compositionId??"unknown"],["Environment",e.environment],["API",e.baseUrl],["Operations",String(t.operations.length)],["SDK package",t.packageName??"not published"]],o=Math.max(...s.map(([r])=>r.length));for(const[r,i]of s)u(`${r.padEnd(o)} ${i}`)}async function Ye(e,n,t){const s=await Y(e),o=t?t.toLowerCase():null,r=o?s.operations.filter(a=>a.name.toLowerCase().includes(o)||a.path.toLowerCase().includes(o)):s.operations;if(n["--json"]){u(JSON.stringify(r,null,2));return}if(r.length===0){u(t?`No operation matches ${t}`:"No operations");return}const i=Math.max(...r.map(a=>a.name.length));for(const a of r)u(`${a.name.padEnd(i)} ${a.method.padEnd(6)}${a.path}`)}function qe(e,n){const t={...n},s=[],o=e.path.replace(/\{([^}]+)\}/g,(r,i)=>{const a=t[i];return a==null?(s.push(i),""):(delete t[i],encodeURIComponent(String(a)))});if(s.length>0)throw new c(`${e.name} needs ${s.join(", ")} in --input`);return{path:o,remaining:t}}function De(e){const n=new URLSearchParams;for(const[s,o]of Object.entries(e))o!=null&&n.set(s,typeof o=="string"?o:JSON.stringify(o));const t=n.toString();return t?`?${t}`:""}async function Ve(e,n,t){if(!t)throw new c("call needs an operation name");const o=(await Y(e)).operations.find(h=>h.name===t||h.operationId===t);if(!o)throw new c(`${t} is not an operation of this product. Run "hyperscale ops list" to see them.`);const r=await Ge(n),{path:i,remaining:a}=qe(o,r),d={method:o.method};let l=i;o.method==="GET"?l+=De(a):(d.body=JSON.stringify(a),d.headers={"content-type":"application/json",...o.idempotency==="not_required"?{}:{"idempotency-key":n["--idempotency-key"]??globalThis.crypto.randomUUID()}});const{response:f,text:w}=await re(e,l,d);if(!f.ok)throw C(f,w,[e.apiKey]);u(We(w))}async function Ge(e){if(e["--input"]!==void 0&&e["--input-file"]!==void 0)throw new c("Use only one of --input and --input-file");if(e["--input"]!==void 0)return G(e["--input"],"--input");if(e["--input-file"]!==void 0){let n;try{n=await L(e["--input-file"],"utf8")}catch(t){throw new c(`Could not read ${e["--input-file"]}: ${t.message}`)}return G(n,"--input-file")}if(!process.stdin.isTTY){const n=await Ce();if(n.trim()!=="")return G(n,"stdin")}return{}}function G(e,n="--input"){if(e===void 0)return{};let t;try{t=JSON.parse(e)}catch(s){throw new c(`${n} is not valid JSON: ${s.message}`)}if(t===null||typeof t!="object"||Array.isArray(t))throw new c(`${n} must be a JSON object`);return t}function We(e){if(e.trim()==="")return"";try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}}function u(e){process.stdout.write(`${e}
|
|
66
|
+
`)}function p(e){u(JSON.stringify(e,null,2))}async function ze(){const{readFileSync:e}=await import("node:fs");return JSON.parse(e(new URL("./package.json",import.meta.url),"utf8")).version}async function Qe(e,n){const{flags:t,positional:s}=$e(e);if(t["--version"])return u(await ze()),0;if(t["--help"]||s.length===0)return process.stdout.write(ke),s.length===0&&!t["--help"]?1:0;const[o,...r]=s;if(o==="auth"){if(r[0]==="login")await Ue(t,n);else if(r[0]==="status")await xe(t,n);else if(r[0]==="logout")await Le(t,n);else throw new c('Auth subcommands are "login", "status", and "logout"');return 0}if(o==="blueprint"){const a=j(t,n);if(r[0]==="list")await Re(a,t);else if(r[0]==="show")await Oe(a,t,r[1]);else throw new c('Blueprint subcommands are "list" and "show"');return 0}if(o==="product"){if(r[0]==="create")await _e(t,n);else if(r[0]==="list")await Je(t,n);else if(r[0]==="show")await Be(t,n,r[1]);else throw new c('Product subcommands are "create", "list", and "show"');return 0}if(o==="key"){if(r[0]==="create")await Fe(t,n);else if(r[0]==="list")await Me(t,n);else if(r[0]==="revoke")await He(t,n,r[1]);else throw new c('Key subcommands are "create", "list", and "revoke"');return 0}const i=be(t,n);if(o==="whoami")return await Ke(i,t),0;if(o==="ops"){if(r[0]!=="list")throw new c('The only ops subcommand is "list"');return await Ye(i,t,r[1]),0}if(o==="call")return await Ve(i,t,r[0]),0;throw new c(`Unknown command: ${o}`)}Qe(process.argv.slice(2),process.env).then(e=>{process.exitCode=e},e=>{process.stderr.write(`${e instanceof c?e.message:e.stack??String(e)}
|
|
32
67
|
`),process.exitCode=1});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperscale0/cli",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
4
|
-
"description": "The Hyperscale command line:
|
|
3
|
+
"version": "1.0.0-alpha.4",
|
|
4
|
+
"description": "The Hyperscale founder command line: compose Products, manage keys, and call Product operations.",
|
|
5
5
|
"license": "LicenseRef-Hyperscale-Proprietary",
|
|
6
6
|
"author": "Hyperscale LLC",
|
|
7
7
|
"contributors": [
|