@hyperscale0/cli 1.0.0-alpha.4 → 1.0.0-alpha.6
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 +14 -4
- package/hyperscale.js +29 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @hyperscale0/cli
|
|
2
2
|
|
|
3
3
|
The Hyperscale founder command line. It logs a founder in, discovers public
|
|
4
|
-
Blueprints, composes Products, manages sandbox API keys, and
|
|
5
|
-
|
|
4
|
+
Blueprints, composes Products, manages sandbox API keys, and runs one Product's
|
|
5
|
+
frozen public verbs.
|
|
6
6
|
|
|
7
7
|
## Use
|
|
8
8
|
|
|
@@ -15,13 +15,23 @@ bunx @hyperscale0/cli product create --blueprint vehicle-escrow-marketplace --na
|
|
|
15
15
|
bunx @hyperscale0/cli key create --name "Founder laptop"
|
|
16
16
|
|
|
17
17
|
export HYPERSCALE_API_KEY=<your Product API key>
|
|
18
|
+
bunx @hyperscale0/cli product verbs
|
|
19
|
+
bunx @hyperscale0/cli product verbs escrow.release --json
|
|
20
|
+
bunx @hyperscale0/cli product run escrow.release --input '{"saleId":"sale_sandbox_..."}'
|
|
18
21
|
bunx @hyperscale0/cli whoami
|
|
19
22
|
bunx @hyperscale0/cli ops list account
|
|
20
23
|
bunx @hyperscale0/cli call account.list --input '{"limit":5}'
|
|
21
24
|
```
|
|
22
25
|
|
|
23
|
-
`
|
|
24
|
-
|
|
26
|
+
`product verbs` lists the frozen Product surface. Pass one exact public
|
|
27
|
+
operation id such as `escrow.release` to return its full input schema, output
|
|
28
|
+
schema, policies, build id, surface version, and operation-set digest.
|
|
29
|
+
`npx @hyperscale0/cli` runs the same commands. Add
|
|
30
|
+
`--json` for machine-readable success and failure output.
|
|
31
|
+
|
|
32
|
+
Product-key commands need no founder login profile. On a clean machine, set
|
|
33
|
+
`HYPERSCALE_API_KEY` and pass `--product <id>`. Use `--base-url` and
|
|
34
|
+
`--environment` when their defaults do not name the Product's plane.
|
|
25
35
|
|
|
26
36
|
Point it at another host with `--base-url`, and at the live plane with
|
|
27
37
|
`--environment live`. Live keys and sandbox keys are never interchangeable.
|
package/hyperscale.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{spawn as
|
|
2
|
+
import{spawn as he}from"node:child_process";import{createHash as me}from"node:crypto";import{chmod as j,mkdir as ye,readdir as ge,readFile as _,rename as $e,unlink as be,writeFile as ke}from"node:fs/promises";import{homedir as Se}from"node:os";import{basename as Pe,dirname as J,join as A}from"node:path";const E="https://hyperscale0.ai",B="sandbox",F="hyperscale-cli-session",Z=process.platform==="darwin"?"/usr/bin/security":null,Ie="config.json",X=3e4,ve=`hyperscale <command> [options]
|
|
3
3
|
|
|
4
4
|
Commands:
|
|
5
5
|
auth login Log in with email, password, and MFA when required
|
|
@@ -10,6 +10,9 @@ Commands:
|
|
|
10
10
|
product create Create a Product from a Blueprint
|
|
11
11
|
product list List Products in the signed-in company
|
|
12
12
|
product show [id] Show a Product (default: selected Product)
|
|
13
|
+
product verbs [operation-id]
|
|
14
|
+
List frozen public verbs or describe one exactly
|
|
15
|
+
product run <operation-id> Run one frozen public verb
|
|
13
16
|
key create Create a sandbox Product API key
|
|
14
17
|
key list List keys for the selected Product
|
|
15
18
|
key revoke <id> Revoke a Product API key
|
|
@@ -19,22 +22,22 @@ Commands:
|
|
|
19
22
|
|
|
20
23
|
Options:
|
|
21
24
|
--api-key <key> Product API key (default: $HYPERSCALE_API_KEY)
|
|
22
|
-
--base-url <url> API origin (default: $HYPERSCALE_BASE_URL, else ${
|
|
23
|
-
--environment <name> sandbox or live (default: $HYPERSCALE_ENVIRONMENT, else ${
|
|
25
|
+
--base-url <url> API origin (default: $HYPERSCALE_BASE_URL, else ${E})
|
|
26
|
+
--environment <name> sandbox or live (default: $HYPERSCALE_ENVIRONMENT, else ${B})
|
|
24
27
|
--email <email> Login email (password and MFA stay on hidden stdin)
|
|
25
28
|
--blueprint <id> Blueprint for product create
|
|
26
29
|
--name <name> Product or key name
|
|
27
|
-
--product <id> Product
|
|
28
|
-
--input <json> JSON input object for call
|
|
29
|
-
--input-file <path> Read call input from a JSON file
|
|
30
|
-
--idempotency-key <key> Idempotency key for a mutation (default: generated)
|
|
30
|
+
--product <id> Product id for key commands and Product-key verbs
|
|
31
|
+
--input <json> JSON input object for call or product run
|
|
32
|
+
--input-file <path> Read call or product run input from a JSON file
|
|
33
|
+
--idempotency-key <key> Idempotency key for a mutation or public verb (default: generated)
|
|
31
34
|
--passkey Print the portal pointer for passkey login
|
|
32
35
|
--json Print machine-readable JSON
|
|
33
36
|
--version Print the CLI version
|
|
34
37
|
--help Print this help
|
|
35
38
|
|
|
36
39
|
Password, MFA code, and user-session bearer never ride in arguments or
|
|
37
|
-
environment variables. Passkey login runs in the portal at ${
|
|
40
|
+
environment variables. Passkey login runs in the portal at ${E}/signin.
|
|
38
41
|
|
|
39
42
|
The Product key rides in the environment, never on the command line, unless
|
|
40
43
|
you are pasting a one-off into a throwaway shell.
|
|
@@ -47,21 +50,26 @@ Examples:
|
|
|
47
50
|
hyperscale blueprint list
|
|
48
51
|
hyperscale product create --blueprint vehicle-escrow-marketplace --name "Vehicle Escrow"
|
|
49
52
|
hyperscale key create --name "Founder laptop"
|
|
53
|
+
hyperscale product verbs
|
|
54
|
+
hyperscale product verbs escrow.release --json
|
|
55
|
+
hyperscale product run escrow.release --input '{"saleId":"sale_sandbox_..."}'
|
|
50
56
|
hyperscale ops list account
|
|
51
57
|
hyperscale call account.list --input '{"limit":5}'
|
|
52
58
|
hyperscale call account.list --input-file input.json
|
|
53
59
|
printf '{"limit":5}' | hyperscale call account.list
|
|
54
60
|
hyperscale call account.retrieve --input '{"accountId":"acct_sandbox_..."}'
|
|
55
|
-
`;class c extends Error{}const
|
|
56
|
-
`)}function
|
|
57
|
-
`)})}function
|
|
58
|
-
`),"file")}async function
|
|
59
|
-
`),{keychainDeleted:!1}):(await
|
|
60
|
-
\r`.includes(n)?n:"")}function
|
|
61
|
-
`),l?t(l):n(d)},i=d=>
|
|
62
|
-
`){
|
|
63
|
-
`)}}}const i=
|
|
64
|
-
`);const f=
|
|
65
|
-
`)}function
|
|
66
|
-
`)}function
|
|
67
|
-
`),process.
|
|
61
|
+
`;class c extends Error{}const Ae=new Set(["--api-key","--base-url","--blueprint","--email","--environment","--idempotency-key","--input","--input-file","--name","--product"]);function Ee(e){const n=[],t={};for(let r=0;r<e.length;r+=1){const o=e[r];if(!o.startsWith("--")){n.push(o);continue}if(Ae.has(o)){const s=e[r+1];if(s===void 0)throw new c(`${o} needs a value`);t[o]=s,r+=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 Te(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 r=e["--environment"]??n.HYPERSCALE_ENVIRONMENT??B;if(r!=="sandbox"&&r!=="live")throw new c(`Unknown environment: ${r}. Use sandbox or live.`);const o=(e["--base-url"]??n.HYPERSCALE_BASE_URL??E).replace(/\/+$/,"");return{apiKey:t,baseUrl:o,environment:r}}function T(e,n,t={}){const r=e["--environment"]??n.HYPERSCALE_ENVIRONMENT??t.environment??B;if(r!=="sandbox"&&r!=="live")throw new c(`Unknown environment: ${r}. Use sandbox or live.`);const o=(e["--base-url"]??n.HYPERSCALE_BASE_URL??t.baseUrl??E).replace(/\/+$/,"");let s;try{s=new URL(o)}catch{s=null}const i=s?.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(s.hostname);if(s.protocol!=="https:"&&!(s.protocol==="http:"&&a))throw new c(`Refusing cleartext API origin ${o}. Use HTTPS unless the API runs on loopback.`);return{baseUrl:o,environment:r}}function ee(e){return e.HYPERSCALE_CONFIG_DIR??A(Se(),".config","hyperscale")}function te(e){return A(ee(e),Ie)}function $(e,n){return A(ee(n),`session-${S(e)}.json`)}async function D(e,n){const t=$(e,n),r=`${Pe(t)}.`;let o;try{o=await ge(J(t),{withFileTypes:!0})}catch(s){if(s&&s.code==="ENOENT")return;throw s}for(const s of o){if(!s.isFile()||!s.name.startsWith(r))continue;const i=s.name.slice(r.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 N(A(J(t),s.name))}}async function Ne(e){try{return JSON.parse(await _(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 K(e,n){const t=Ue(e);await ye(t,{mode:448,recursive:!0}),await j(t,448);const r=`${e}.${process.pid}.${globalThis.crypto.randomUUID()}.tmp`;await ke(r,n,{flag:"wx",mode:384}),await j(r,384);try{await $e(r,e),await j(e,384)}catch(o){throw await N(r),o}}function Ue(e){return J(e)}async function N(e){try{await be(e)}catch(n){if(!n||n.code!=="ENOENT")throw n}}function Ce(e,n){if(!e||typeof e!="object"||Array.isArray(e))throw new c(`${n} does not contain a CLI profile`);const{baseUrl:t,email:r,environment:o,selectedProduct:s}=e;if(typeof t!="string"||typeof r!="string"||o!=="sandbox"&&o!=="live"||!(s===null||typeof s=="string"))throw new c(`${n} does not contain a valid CLI profile`);return{baseUrl:t,email:r,environment:o,selectedProduct:s}}async function v(e){const n=te(e),t=await Ne(n);return t===null?null:Ce(t,n)}async function ne(e,n){const t={email:n.email,baseUrl:n.baseUrl,environment:n.environment,selectedProduct:n.selectedProduct??null};await K(te(e),`${JSON.stringify(t,null,2)}
|
|
62
|
+
`)}function S(e){return me("sha256").update(`${e.baseUrl}\0${e.environment}\0${e.email}`).digest("hex")}async function H(e,n){return Z===null?{error:null,status:null,stdout:"",unavailable:!0}:await new Promise(t=>{const r=he(Z,e,{stdio:["pipe","pipe","pipe"]}),o=[];r.stdout.on("data",s=>o.push(s)),r.stderr.on("data",()=>{}),r.once("error",s=>t({error:s,status:null,stdout:"",unavailable:s?.code==="ENOENT"})),r.once("close",s=>t({error:null,status:s,stdout:Buffer.concat(o).toString("utf8"),unavailable:!1})),n===void 0?r.stdin.end():r.stdin.end(`${n}
|
|
63
|
+
`)})}function re(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 Oe(e,n,t){await D(e,t);const r=S(e),o=JSON.stringify(n),s=["add-generic-password","-a",r,"-s",F,"-U","-X",Buffer.from(o,"utf8").toString("hex")].join(" ");return(await H(["-i"],s)).status===0?(await N($(e,t)),"keychain"):(await K($(e,t),`${JSON.stringify({account:r,credential:n})}
|
|
64
|
+
`),"file")}async function xe(e,n){const t=S(e);try{const r=JSON.parse(await _($(e,n),"utf8"));if(!r||typeof r!="object"||Array.isArray(r)||r.account!==t)return{state:"invalid"};const o=re(r.credential);return o?{credential:o,state:"valid"}:{state:"invalid"}}catch(r){return r&&r.code==="ENOENT"?{state:"missing"}:{state:"invalid"}}}async function M(e,n){await D(e,n);const t=await xe(e,n);if(t.state==="valid")return t.credential;if(t.state==="invalid")return null;const r=await H(["find-generic-password","-a",S(e),"-s",F,"-w"]);if(r.status===0){const o=re(r.stdout.trim());if(o)return o}return null}async function Y(e,n){await D(e,n);const t=S(e),r=await H(["delete-generic-password","-a",S(e),"-s",F]);return!r.unavailable&&r.status!==0&&r.status!==44?(await K($(e,n),`${JSON.stringify({account:t,revoked:!0})}
|
|
65
|
+
`),{keychainDeleted:!1}):(await N($(e,n)),{keychainDeleted:!0})}async function P(e,n){const t=await v(n);if(!t)throw new c('Not logged in. Run "hyperscale auth login".');const r=T(e,n,t);if(r.baseUrl!==t.baseUrl||r.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 oe(e){return e.replace(/\p{Cc}/gu,n=>`
|
|
66
|
+
\r`.includes(n)?n:"")}function U(e){try{return new URL(e).origin}catch{return null}}function b(e,n){let t=e;for(const r of n)typeof r=="string"&&r.length>0&&(t=t.replaceAll(r,"[redacted]"));return t}async function se(e,n,t={},r){const o=`${e.baseUrl}${n}`,s=U(e.baseUrl);if(U(o)!==s)throw new c(`The requested path ${n} leaves the ${s} origin; refusing the request.`);try{const i=await fetch(o,{...t,headers:{accept:"application/json",...r?{authorization:`Bearer ${r}`}:{},"x-hyperscale-environment":e.environment,...t.headers},signal:AbortSignal.timeout(X),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:oe(await i.text())}}catch(i){throw i instanceof c?i:new c(`Could not reach ${o}: ${i.message}`)}}async function y(e,n,t={},r,o=[]){const{response:s,text:i}=await se(e,n,t,r);if(!s.ok)throw O(s,i,o);try{return JSON.parse(i)}catch{throw new c(`HTTP ${s.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 h="";async function q(e,n=!1){return process.stderr.write(e),process.stdin.isTTY&&typeof process.stdin.setRawMode=="function"?await Le(n):await Re()}async function Re(){const e=ie();if(e!==null)return e;if(process.stdin.readableEnded){const n=h.replace(/\r$/,"");return h="",n}return await new Promise((n,t)=>{const r=a=>{h+=a.toString("utf8");const d=ie();d!==null&&(i(),n(d))},o=()=>{const a=h.replace(/\r$/,"");h="",i(),n(a)},s=a=>{i(),t(a)},i=()=>{process.stdin.off("data",r),process.stdin.off("end",o),process.stdin.off("error",s),process.stdin.pause()};process.stdin.on("data",r),process.stdin.once("end",o),process.stdin.once("error",s),process.stdin.resume()})}function ie(){const e=/\r?\n/.exec(h);if(!e||e.index===void 0)return null;const n=h.slice(0,e.index);return h=h.slice(e.index+e[0].length),n}async function Le(e){return await new Promise((n,t)=>{const r=process.stdin.isRaw;let o="";const s=(d,l)=>{process.stdin.off("data",a),process.stdin.off("error",i),process.stdin.setRawMode(!!r),process.stdin.pause(),process.stderr.write(`
|
|
67
|
+
`),l?t(l):n(d)},i=d=>s("",d),a=d=>{for(const l of d.toString("utf8")){if(l===""){s("",new c("Login cancelled"));return}if(l==="\r"||l===`
|
|
68
|
+
`){s(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 je(){const e=[];for await(const n of process.stdin)e.push(Buffer.from(n));return Buffer.concat(e).toString("utf8")}async function C(e,n,t={}){const r=`${e.baseUrl}${n}`,o=U(e.baseUrl);if(U(r)!==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 s=await fetch(r,{...t,headers:{accept:"application/json",authorization:`Bearer ${e.apiKey}`,"x-hyperscale-environment":e.environment,...t.headers},signal:AbortSignal.timeout(X),redirect:"manual"});if(s.status>=300&&s.status<400)throw new c(`${r} 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:s,text:oe(await s.text())}}catch(s){throw s instanceof c?s:new c(`Could not reach ${r}: ${s.message}`)}}async function V(e){const{response:n,text:t}=await C(e,"/v1/llms.txt");if(!n.ok)throw O(n,t,[e.apiKey]);const r=/^# (.+)$/m.exec(t),o=/^> Generated for composition (\S+) /m.exec(t),s=/^## Operations \((\d+)\)$/m.exec(t);if(!r||!s)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(s[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:r[1].trim()}}function O(e,n,t=[]){let r=b(n.trim(),t);try{const o=JSON.parse(n);o&&o.error&&typeof o.error.message=="string"&&(r=b(`${o.error.code}: ${o.error.message}`,t))}catch{}return new c(`HTTP ${e.status}${r?` \xB7 ${r}`:""}`)}function ae(e,n){return{authenticated:n,baseUrl:e.baseUrl,email:e.email,environment:e.environment,selectedProduct:e.selectedProduct}}function _e(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 Je(e,n){const t=T(e,n);if(e["--passkey"]){u(`Passkey login runs in the portal: ${t.baseUrl}/signin`);return}const r=e["--email"]??(await q("Email: ",!1)).trim();if(!r)throw new c("Login email is required");const o=await q("Password: ",!0);if(!o)throw new c("Password is required");let s=await y(t,"/v1/identity/login",I({email:r,password:o}),void 0,[o]);if(s?.status==="mfa_required"){const w=s.mfaChallengeToken;if(typeof w!="string"||w.length===0)throw new c(`MFA must continue in the portal: ${t.baseUrl}/signin`);for(;s?.status!=="authenticated";){const m=await q("MFA code: ",!0);if(!m)throw new c("MFA code is required");if(s=await y(t,"/v1/identity/login/mfa/verify",I({code:m,mfaChallengeToken:w}),void 0,[o,w,m]),s?.status==="denied"){const g=s.attemptsRemaining;if(typeof g!="number"||g<=0)throw new c(`MFA was denied. Continue in the portal: ${t.baseUrl}/signin`);process.stderr.write(`MFA denied. ${g} attempts remain.
|
|
69
|
+
`)}}}const i=_e(s,t.baseUrl),a=await v(n),d={email:r,...t,selectedProduct:a&&a.email===r&&a.baseUrl===t.baseUrl&&a.environment===t.environment?a.selectedProduct:null},l=await Oe(d,{sessionToken:i.sessionToken,tenantId:i.tenantId},n);try{await ne(n,d)}catch(w){throw await Y(d,n),w}l==="file"&&process.stderr.write(`macOS Keychain is unavailable. The session is in ${$(d,n)} with mode 0600.
|
|
70
|
+
`);const f=ae(d,!0);e["--json"]?p(f):u(`Logged in as ${r} to ${t.baseUrl} (${t.environment}).`)}async function Be(e,n){const t=await v(n);if(!t){const s={authenticated:!1};e["--json"]?p(s):u("Not logged in.");return}const r=await M(t,n),o=ae(t,r!==null);if(e["--json"]){p(o);return}u(r?`Logged in as ${t.email}.`:"Session credential missing."),u(`API: ${t.baseUrl}`),u(`Environment: ${t.environment}`),u(`Product: ${t.selectedProduct??"not selected"}`)}async function Fe(e,n){const t=await v(n);if(!t){e["--json"]?p({status:"signed_out"}):u("Already logged out.");return}const r=await M(t,n);if(!r){const d=await Y(t,n);z(e,{cleanup:d,message:"Already logged out.",status:"signed_out"});return}const{response:o,text:s}=await se(t,"/v1/identity/logout",I({}),r.sessionToken);if(!o.ok&&o.status!==401&&o.status!==403)throw O(o,s,[r.sessionToken]);const i=await Y(t,n);if(o.status===401||o.status===403){z(e,{cleanup:i,message:"The server had already rejected this session.",serverStatus:o.status,status:"signed_out"});return}let a={};try{a=JSON.parse(s)}catch{}z(e,{cleanup:i,message:"The server revoked the session.",revokedAt:a.revokedAt??null,status:a.status??"revoked"})}function z(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.
|
|
71
|
+
`)}function x(e,n){if(!e||typeof e!="object"||!Array.isArray(e.items))throw new c(`${n} returned an invalid collection`);return e.items}async function G(e,n,t,r,o,s=[]){const i=[],a=new Set;let d,l;for(;;){const g=new URLSearchParams(t);d&&g.set("cursor",d);const we=g.size>0?`?${g}`:"",L=await y(e,`${n}${we}`,{},o,s);l===void 0&&(l=L),i.push(...x(L,r));const k=L.nextCursor;if(typeof k!="string"||k.length===0)break;if(a.has(k))throw new c(`${r} repeated cursor ${k}`);a.add(k),d=k}const{items:f,nextCursor:w,...m}=l;return{...m,items:i}}async function De(e,n){const t=await G(e,"/v1/blueprints",{},"blueprint.list"),r=x(t,"blueprint.list");if(n["--json"]){p(t);return}for(const o of r)u(`${o.id} ${o.name}${o.summary?` ${o.summary}`:""}`)}async function Ke(e,n,t){if(!t)throw new c("blueprint show needs a Blueprint id");const r=await y(e,`/v1/blueprints/${encodeURIComponent(t)}`);p(r)}async function He(e,n){const t=e["--blueprint"],r=e["--name"];if(!t)throw new c("product create needs --blueprint <id>");if(!r)throw new c("product create needs --name <name>");const o=await P(e,n),s=await y(o,"/v1/workspaces/blueprint-compose",I({blueprintId:t,name:r,tenantId:o.tenantId}),o.sessionToken,[o.sessionToken]);if(typeof s?.workspaceId!="string"||typeof s?.blueprintId!="string"||typeof s?.workspaceBuildId!="string")throw new c("workspace.blueprint.compose returned an invalid Product");await ne(n,{...o,selectedProduct:s.workspaceId});const i={blueprintId:s.blueprintId,nextSteps:[`hyperscale product show ${s.workspaceId}`,'hyperscale key create --name "Founder CLI"'],workspaceBuildId:s.workspaceBuildId,workspaceId:s.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 Me(e){return new URLSearchParams({tenantId:e}).toString()}async function Ye(e,n){const t=await P(e,n),r=await G(t,"/v1/products",{tenantId:t.tenantId},"product.list",t.sessionToken,[t.sessionToken]),o=x(r,"product.list");if(e["--json"]){p(r);return}for(const s of o){const i=s.workspaceId??s.id??"unknown",a=s.displayName??s.name??"unnamed";u(`${i} ${a}${s.status?` ${s.status}`:""}`)}}async function qe(e,n,t){const r=await P(e,n),o=t??r.selectedProduct;if(!o)throw new c("product show needs an id because no Product is selected. Create a Product first.");const s=await y(r,`/v1/products/${encodeURIComponent(o)}?${Me(r.tenantId)}`,{},r.sessionToken,[r.sessionToken]);p(s)}function R(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 ce(e,n){const t=await v(n),r=R(t??{},e),o=T(e,n,t??{});if(t&&(o.baseUrl!==t.baseUrl||o.environment!==t.environment))throw new c(`The selected Product belongs to ${t.baseUrl} (${t.environment}). Select a Product at the configured API origin first.`);const s=e["--api-key"]??n.HYPERSCALE_API_KEY;if(!s)throw new c("No API key. Set HYPERSCALE_API_KEY, or pass --api-key. Request access at https://hyperscale0.ai/request-access.");return{...o,apiKey:s,workspaceId:r}}async function Ve(e,n,t){const r=await ce(e,n),o=`/v1/workspaces/${encodeURIComponent(r.workspaceId)}/operation-set`,{response:s,text:i}=await C(r,o);if(!s.ok)throw de(s,i,[r.apiKey]);const a=Ge(i,s.status);if(t){ze(a,t,e);return}if(e["--json"]){p(a);return}if(a.status==="legacy"){u("This Product uses a legacy surface. It has no frozen public verbs yet."),u(`Build: ${a.workspaceBuildId}`);return}if(a.publicVerbs.length===0)u("No public verbs");else{u("Operation ID Input Kind");for(const d of a.publicVerbs){const l=d.idempotencyPolicy?.mode==="not_required"?"read-only":"mutation";u(`${d.noun}.${d.name} ${We(d.inputSchema)} ${l}`)}}u(`Digest: ${a.tenantOperationSetDigest}`),u(`Build: ${a.workspaceBuildId}`),u(`Surface version: ${a.tenantSurfaceVersion}`)}function ze(e,n,t){if(e.status==="legacy")throw new c(`This Product uses a legacy surface and has no frozen public verb named ${n}. Run "hyperscale product verbs" after rebuilding the Product.`);const r=e.publicVerbs.find(s=>s&&`${s.noun}.${s.name}`===n);if(!r)throw new c(`Unknown frozen public verb: ${n}. Run "hyperscale product verbs" to list exact names.`);const o={status:e.status,workspaceBuildId:e.workspaceBuildId,tenantSurfaceVersion:e.tenantSurfaceVersion,tenantOperationSetDigest:e.tenantOperationSetDigest,publicVerb:r};if(t["--json"]){p(o);return}u(`Operation ID: ${r.noun}.${r.name}`),u(`Noun: ${r.noun}`),u(`Input schema: ${JSON.stringify(r.inputSchema,null,2)}`),u(`Output schema: ${JSON.stringify(r.outputSchema,null,2)}`),u(`Actor policy: ${JSON.stringify(r.actorPolicy,null,2)}`),u(`Port policy: ${JSON.stringify(r.portPolicy,null,2)}`),u(`Idempotency policy: ${JSON.stringify(r.idempotencyPolicy,null,2)}`),u(`Receipt policy: ${JSON.stringify(r.receiptPolicy,null,2)}`),u(`Receipt evidence: ${JSON.stringify(r.receiptEvidence,null,2)}`),u(`Digest: ${e.tenantOperationSetDigest}`),u(`Build: ${e.workspaceBuildId}`),u(`Surface version: ${e.tenantSurfaceVersion}`)}function Ge(e,n){let t;try{t=JSON.parse(e)}catch{throw new c(`HTTP ${n} returned invalid JSON`)}if(!t||typeof t!="object"||t.status!=="legacy"&&t.status!=="frozen"||typeof t.workspaceBuildId!="string")throw new c("The Product operation set is not in the expected format. Upgrade the CLI.");if(t.status==="legacy")return t;if(typeof t.tenantSurfaceVersion!="number"||typeof t.tenantOperationSetDigest!="string"||!Array.isArray(t.publicVerbs))throw new c("The Product operation set is not in the expected format. Upgrade the CLI.");return t}function We(e){if(!e||typeof e!="object")return"input schema unavailable";const n=e.properties&&typeof e.properties=="object"&&!Array.isArray(e.properties)?e.properties:{},t=new Set(Array.isArray(e.required)?e.required.filter(o=>typeof o=="string"):[]),r=Object.entries(n).map(([o,s])=>`${o}${t.has(o)?"":"?"}: ${ue(s)}`);return r.length===0?"no input":r.join(", ")}function ue(e){return!e||typeof e!="object"?"value":Array.isArray(e.enum)&&e.enum.length>0?e.enum.map(n=>String(n)).join(" | "):e.type==="array"?`${ue(e.items)}[]`:typeof e.type=="string"?typeof e.format=="string"?`${e.type} (${e.format})`:e.type:"value"}async function Qe(e,n,t){if(!t)throw new c("product run needs a public verb");const r=await ce(e,n),o=await pe(e),{response:s,text:i}=await C(r,`/v1/operations/${encodeURIComponent(t)}`,{body:JSON.stringify(o),headers:{"content-type":"application/json","idempotency-key":e["--idempotency-key"]??globalThis.crypto.randomUUID()},method:"POST"});if(!s.ok)throw de(s,i,[r.apiKey]);const a=s.headers.get("hyperscale-operation-id"),d=s.headers.get("hyperscale-receipt-id");if(e["--json"]){p({operationId:a,receiptId:d,output:Ze(i)});return}u(`Operation: ${a??"not returned"}`),u(`Receipt: ${d??"not returned"}`);const l=fe(i);l&&u(l)}function Ze(e){if(e.trim()==="")return null;try{return JSON.parse(e)}catch{return e}}function de(e,n,t=[]){const r=b(n.trim(),t);let o;try{o=JSON.parse(n)}catch{return new c(`HTTP ${e.status}${r?` \xB7 ${r}`:""}`)}if(!o?.error||typeof o.error.message!="string")return new c(`HTTP ${e.status}`);const s=typeof o.error.code=="string"?`${o.error.code}: `:"",i=b(`HTTP ${e.status} \xB7 ${s}${o.error.message}`,t),a=Xe(o.error.details,t);return new c([i,...a].join(`
|
|
72
|
+
`))}function Xe(e,n){if(!e||typeof e!="object")return[];if(Array.isArray(e.issues))return e.issues.flatMap(r=>{if(!r||typeof r!="object")return[];const s=(Array.isArray(r.path)?r.path.join("."):"input")||"input";return typeof r.message=="string"?[`${s}: ${b(r.message,n)}`]:[]});const t=[];return W(e,"",t,n),t}function W(e,n,t,r){if(Array.isArray(e)){if(e.every(o=>o===null||typeof o!="object")){t.push(`${le(n)}: ${b(e.join(", "),r)}`);return}e.forEach((o,s)=>W(o,`${n}[${s}]`,t,r));return}if(e&&typeof e=="object"){for(const[o,s]of Object.entries(e))W(s,n?`${n}.${o}`:o,t,r);return}n&&t.push(`${le(n)}: ${b(String(e),r)}`)}function le(e){const n=e.replace(/([a-z0-9])([A-Z])/g,"$1 $2").replaceAll("."," ");return n.charAt(0).toUpperCase()+n.slice(1)}async function et(e,n){const t=await P(e,n),r=R(t,e),o=await y(t,"/v1/sandbox-api-keys",I({name:e["--name"]??"Founder CLI",tenantId:t.tenantId,workspaceId:r}),t.sessionToken,[t.sessionToken]);if(e["--json"]){p(o);return}u(`Key: ${o.apiKeyId??"unknown"}`),u(`Product: ${o.workspaceId??r}`),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 tt(e,n){const t=await P(e,n),r=R(t,e),o=await G(t,"/v1/api-keys",{tenantId:t.tenantId},"api_key.list",t.sessionToken,[t.sessionToken]),s=x(o,"api_key.list").filter(a=>a.workspaceId===r),i={...o,items:s};if(e["--json"]){p(i);return}for(const a of s)u(`${a.apiKeyId} ${a.name??"unnamed"} ${a.status??"unknown"}`)}async function nt(e,n,t){if(!t)throw new c("key revoke needs an API key id");const r=await P(e,n),o=R(r,e),s=await y(r,`/v1/api-keys/${encodeURIComponent(t)}/revoke`,I({tenantId:r.tenantId,workspaceId:o}),r.sessionToken,[r.sessionToken]);e["--json"]?p(s):u(`Key ${s.apiKeyId??t}: ${s.status??"revoked"}`)}async function rt(e,n){const t=await V(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 r=[["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(...r.map(([s])=>s.length));for(const[s,i]of r)u(`${s.padEnd(o)} ${i}`)}async function ot(e,n,t){const r=await V(e),o=t?t.toLowerCase():null,s=o?r.operations.filter(a=>a.name.toLowerCase().includes(o)||a.path.toLowerCase().includes(o)):r.operations;if(n["--json"]){u(JSON.stringify(s,null,2));return}if(s.length===0){u(t?`No operation matches ${t}`:"No operations");return}const i=Math.max(...s.map(a=>a.name.length));for(const a of s)u(`${a.name.padEnd(i)} ${a.method.padEnd(6)}${a.path}`)}function st(e,n){const t={...n},r=[],o=e.path.replace(/\{([^}]+)\}/g,(s,i)=>{const a=t[i];return a==null?(r.push(i),""):(delete t[i],encodeURIComponent(String(a)))});if(r.length>0)throw new c(`${e.name} needs ${r.join(", ")} in --input`);return{path:o,remaining:t}}function it(e){const n=new URLSearchParams;for(const[r,o]of Object.entries(e))o!=null&&n.set(r,typeof o=="string"?o:JSON.stringify(o));const t=n.toString();return t?`?${t}`:""}async function at(e,n,t){if(!t)throw new c("call needs an operation name");const o=(await V(e)).operations.find(m=>m.name===t||m.operationId===t);if(!o)throw new c(`${t} is not an operation of this product. Run "hyperscale ops list" to see them.`);const s=await pe(n),{path:i,remaining:a}=st(o,s),d={method:o.method};let l=i;o.method==="GET"?l+=it(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 C(e,l,d);if(!f.ok)throw O(f,w,[e.apiKey]);u(fe(w))}async function pe(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 Q(e["--input"],"--input");if(e["--input-file"]!==void 0){let n;try{n=await _(e["--input-file"],"utf8")}catch(t){throw new c(`Could not read ${e["--input-file"]}: ${t.message}`)}return Q(n,"--input-file")}if(!process.stdin.isTTY){const n=await je();if(n.trim()!=="")return Q(n,"stdin")}return{}}function Q(e,n="--input"){if(e===void 0)return{};let t;try{t=JSON.parse(e)}catch(r){throw new c(`${n} is not valid JSON: ${r.message}`)}if(t===null||typeof t!="object"||Array.isArray(t))throw new c(`${n} must be a JSON object`);return t}function fe(e){if(e.trim()==="")return"";try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}}function u(e){process.stdout.write(`${e}
|
|
73
|
+
`)}function p(e){u(JSON.stringify(e,null,2))}function ct(e,n){if(!n.includes("--json")){process.stderr.write(`${e instanceof c?e.message:e.stack??String(e)}
|
|
74
|
+
`);return}const t=e instanceof c;process.stderr.write(`${JSON.stringify({error:{code:t?"user_error":"internal_error",message:t?e.message:"The command failed unexpectedly.",fix:t?'Follow the message, or run "hyperscale --help" for command usage.':"Report the failure without including credentials."}})}
|
|
75
|
+
`)}async function ut(){const{readFileSync:e}=await import("node:fs");return JSON.parse(e(new URL("./package.json",import.meta.url),"utf8")).version}async function dt(e,n){const{flags:t,positional:r}=Ee(e);if(t["--version"])return u(await ut()),0;if(t["--help"]||r.length===0){if(r.length===0&&!t["--help"]&&t["--json"])throw new c('No command. Run "hyperscale --help" to list commands.');return process.stdout.write(ve),r.length===0&&!t["--help"]?1:0}const[o,...s]=r;if(o==="auth"){if(s[0]==="login")await Je(t,n);else if(s[0]==="status")await Be(t,n);else if(s[0]==="logout")await Fe(t,n);else throw new c('Auth subcommands are "login", "status", and "logout"');return 0}if(o==="blueprint"){const a=T(t,n);if(s[0]==="list")await De(a,t);else if(s[0]==="show")await Ke(a,t,s[1]);else throw new c('Blueprint subcommands are "list" and "show"');return 0}if(o==="product"){if(s[0]==="create")await He(t,n);else if(s[0]==="list")await Ye(t,n);else if(s[0]==="show")await qe(t,n,s[1]);else if(s[0]==="verbs")await Ve(t,n,s[1]);else if(s[0]==="run")await Qe(t,n,s[1]);else throw new c('Product subcommands are "create", "list", "show", "verbs", and "run"');return 0}if(o==="key"){if(s[0]==="create")await et(t,n);else if(s[0]==="list")await tt(t,n);else if(s[0]==="revoke")await nt(t,n,s[1]);else throw new c('Key subcommands are "create", "list", and "revoke"');return 0}const i=Te(t,n);if(o==="whoami")return await rt(i,t),0;if(o==="ops"){if(s[0]!=="list")throw new c('The only ops subcommand is "list"');return await ot(i,t,s[1]),0}if(o==="call")return await at(i,t,s[0]),0;throw new c(`Unknown command: ${o}`)}dt(process.argv.slice(2),process.env).then(e=>{process.exitCode=e},e=>{ct(e,process.argv.slice(2)),process.exitCode=1});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperscale0/cli",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.6",
|
|
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",
|