@hyperscale0/cli 1.0.0-alpha.3 → 1.0.0-alpha.5

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 +14 -4
  2. package/hyperscale.js +53 -13
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,16 +1,22 @@
1
1
  # @hyperscale0/cli
2
2
 
3
- The Hyperscale command line. It drives one Product's operation surface with a
4
- Product API key, and it learns that surface live from the Product itself, so it
5
- never drifts from the contract.
3
+ The Hyperscale founder command line. It logs a founder in, discovers public
4
+ Blueprints, composes Products, manages sandbox API keys, and runs one Product's
5
+ frozen public verbs.
6
6
 
7
7
  ## Use
8
8
 
9
9
  Request access at https://hyperscale0.ai/request-access.
10
10
 
11
11
  ```bash
12
- export HYPERSCALE_API_KEY=<your Product API key>
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>
18
+ bunx @hyperscale0/cli product verbs
19
+ bunx @hyperscale0/cli product run escrow.release --input '{"saleId":"sale_sandbox_..."}'
14
20
  bunx @hyperscale0/cli whoami
15
21
  bunx @hyperscale0/cli ops list account
16
22
  bunx @hyperscale0/cli call account.list --input '{"limit":5}'
@@ -21,5 +27,9 @@ machine-readable output.
21
27
 
22
28
  Point it at another host with `--base-url`, and at the live plane with
23
29
  `--environment live`. Live keys and sandbox keys are never interchangeable.
30
+ Password, MFA code, and user-session bearer never use arguments or environment
31
+ variables. The CLI stores the session in macOS Keychain, with a 0600 file
32
+ fallback on systems where Keychain is unavailable. Passkey login stays in the
33
+ portal at https://hyperscale0.ai/signin.
24
34
 
25
35
  Run `bunx @hyperscale0/cli --help` for the full option list.
package/hyperscale.js CHANGED
@@ -1,32 +1,72 @@
1
1
  #!/usr/bin/env node
2
- const DEFAULT_BASE_URL="https://hyperscale0.ai",DEFAULT_ENVIRONMENT="sandbox",REQUEST_TIMEOUT_MS=3e4,USAGE=`hyperscale <command> [options]
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 be,unlink as $e,writeFile as ke}from"node:fs/promises";import{homedir as Se}from"node:os";import{basename as Pe,dirname as J,join as v}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,Ae=`hyperscale <command> [options]
3
3
 
4
4
  Commands:
5
- whoami Show the product this API key resolves to
6
- ops list [filter] List the product's callable operations
7
- call <operation> Call one operation by name or operation id
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
+ product verbs Show the selected Product's frozen public verbs
14
+ product run <verb> Run one frozen public verb
15
+ key create Create a sandbox Product API key
16
+ key list List keys for the selected Product
17
+ key revoke <id> Revoke a Product API key
18
+ whoami Show the product this API key resolves to
19
+ ops list [filter] List the product's callable operations
20
+ call <operation> Call one operation by name or operation id
8
21
 
9
22
  Options:
10
23
  --api-key <key> Product API key (default: $HYPERSCALE_API_KEY)
11
- --base-url <url> API origin (default: $HYPERSCALE_BASE_URL, else ${DEFAULT_BASE_URL})
12
- --environment <name> sandbox or live (default: $HYPERSCALE_ENVIRONMENT, else ${DEFAULT_ENVIRONMENT})
13
- --input <json> JSON input object for call
14
- --idempotency-key <key> Idempotency key for a mutation (default: generated)
24
+ --base-url <url> API origin (default: $HYPERSCALE_BASE_URL, else ${E})
25
+ --environment <name> sandbox or live (default: $HYPERSCALE_ENVIRONMENT, else ${B})
26
+ --email <email> Login email (password and MFA stay on hidden stdin)
27
+ --blueprint <id> Blueprint for product create
28
+ --name <name> Product or key name
29
+ --product <id> Product override for key commands
30
+ --input <json> JSON input object for call or product run
31
+ --input-file <path> Read call or product run input from a JSON file
32
+ --idempotency-key <key> Idempotency key for a mutation or public verb (default: generated)
33
+ --passkey Print the portal pointer for passkey login
15
34
  --json Print machine-readable JSON
16
35
  --version Print the CLI version
17
36
  --help Print this help
18
37
 
19
- The key rides in the environment, never on the command line, unless you are
20
- pasting a one-off into a throwaway shell.
38
+ Password, MFA code, and user-session bearer never ride in arguments or
39
+ environment variables. Passkey login runs in the portal at ${E}/signin.
40
+
41
+ The Product key rides in the environment, never on the command line, unless
42
+ you are pasting a one-off into a throwaway shell.
21
43
 
22
44
  Request access at https://hyperscale0.ai/request-access.
23
45
 
24
46
  Examples:
25
47
  hyperscale whoami
48
+ hyperscale auth login --email founder@example.com
49
+ hyperscale blueprint list
50
+ hyperscale product create --blueprint vehicle-escrow-marketplace --name "Vehicle Escrow"
51
+ hyperscale key create --name "Founder laptop"
52
+ hyperscale product verbs
53
+ hyperscale product run escrow.release --input '{"saleId":"sale_sandbox_..."}'
26
54
  hyperscale ops list account
27
55
  hyperscale call account.list --input '{"limit":5}'
56
+ hyperscale call account.list --input-file input.json
57
+ printf '{"limit":5}' | hyperscale call account.list
28
58
  hyperscale call account.retrieve --input '{"accountId":"acct_sandbox_..."}'
29
- `;class UserError extends Error{}const FLAGS_WITH_VALUES=new Set(["--api-key","--base-url","--environment","--idempotency-key","--input"]);function parseArgv(e){const r=[],t={};for(let o=0;o<e.length;o+=1){const n=e[o];if(!n.startsWith("--")){r.push(n);continue}if(FLAGS_WITH_VALUES.has(n)){const s=e[o+1];if(s===void 0)throw new UserError(`${n} needs a value`);t[n]=s,o+=1;continue}if(n==="--help"||n==="--version"||n==="--json"){t[n]=!0;continue}throw new UserError(`Unknown option: ${n}`)}return{flags:t,positional:r}}function resolveConfig(e,r){const t=e["--api-key"]??r.HYPERSCALE_API_KEY;if(!t)throw new UserError("No API key. Set HYPERSCALE_API_KEY, or pass --api-key. Request access at https://hyperscale0.ai/request-access.");const o=e["--environment"]??r.HYPERSCALE_ENVIRONMENT??DEFAULT_ENVIRONMENT;if(o!=="sandbox"&&o!=="live")throw new UserError(`Unknown environment: ${o}. Use sandbox or live.`);const n=(e["--base-url"]??r.HYPERSCALE_BASE_URL??DEFAULT_BASE_URL).replace(/\/+$/,"");return{apiKey:t,baseUrl:n,environment:o}}function withoutControlCharacters(e){return e.replace(/\p{Cc}/gu,r=>`
30
- \r`.includes(r)?r:"")}function originOf(e){try{return new URL(e).origin}catch{return null}}async function request(e,r,t={}){const o=`${e.baseUrl}${r}`,n=originOf(e.baseUrl);if(originOf(o)!==n)throw new UserError(`The product document puts ${r} on another origin than the ${n} you configured; refusing to send the key there.`);try{const s=await fetch(o,{...t,headers:{accept:"application/json",authorization:`Bearer ${e.apiKey}`,"x-hyperscale-environment":e.environment,...t.headers},signal:AbortSignal.timeout(3e4),redirect:"manual"});if(s.status>=300&&s.status<400)throw new UserError(`${o} 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:withoutControlCharacters(await s.text())}}catch(s){throw s instanceof UserError?s:new UserError(`Could not reach ${o}: ${s.message}`)}}async function readProduct(e){const{response:r,text:t}=await request(e,"/v1/llms.txt");if(!r.ok)throw apiError(r,t);const o=/^# (.+)$/m.exec(t),n=/^> Generated for composition (\S+) /m.exec(t),s=/^## Operations \((\d+)\)$/m.exec(t);if(!o||!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 c of t.matchAll(a))i.push({idempotency:c[5],method:c[1],name:c[3],operationId:c[4],path:c[2]});const p=Number(s[1]);if(i.length!==p)throw new Error(`The product document declares ${p} operations but ${i.length} parsed; upgrade the CLI.`);const u=/^- Package: (.+)$/m.exec(t);return{compositionId:n?n[1]:null,operations:i,packageName:u?u[1].trim():null,title:o[1].trim()}}function apiError(e,r){let t=r.trim();try{const o=JSON.parse(r);o&&o.error&&typeof o.error.message=="string"&&(t=`${o.error.code}: ${o.error.message}`)}catch{}return new UserError(`HTTP ${e.status}${t?` \xB7 ${t}`:""}`)}async function commandWhoami(e,r){const t=await readProduct(e);if(r["--json"]){print(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 o=[["Product",t.title],["Composition",t.compositionId??"unknown"],["Environment",e.environment],["API",e.baseUrl],["Operations",String(t.operations.length)],["SDK package",t.packageName??"not published"]],n=Math.max(...o.map(([s])=>s.length));for(const[s,i]of o)print(`${s.padEnd(n)} ${i}`)}async function commandOpsList(e,r,t){const o=await readProduct(e),n=t?t.toLowerCase():null,s=n?o.operations.filter(a=>a.name.toLowerCase().includes(n)||a.path.toLowerCase().includes(n)):o.operations;if(r["--json"]){print(JSON.stringify(s,null,2));return}if(s.length===0){print(t?`No operation matches ${t}`:"No operations");return}const i=Math.max(...s.map(a=>a.name.length));for(const a of s)print(`${a.name.padEnd(i)} ${a.method.padEnd(6)}${a.path}`)}function bindPath(e,r){const t={...r},o=[],n=e.path.replace(/\{([^}]+)\}/g,(s,i)=>{const a=t[i];return a==null?(o.push(i),""):(delete t[i],encodeURIComponent(String(a)))});if(o.length>0)throw new UserError(`${e.name} needs ${o.join(", ")} in --input`);return{path:n,remaining:t}}function queryString(e){const r=new URLSearchParams;for(const[o,n]of Object.entries(e))n!=null&&r.set(o,typeof n=="string"?n:JSON.stringify(n));const t=r.toString();return t?`?${t}`:""}async function commandCall(e,r,t){if(!t)throw new UserError("call needs an operation name");const n=(await readProduct(e)).operations.find(h=>h.name===t||h.operationId===t);if(!n)throw new UserError(`${t} is not an operation of this product. Run "hyperscale ops list" to see them.`);const s=parseInput(r["--input"]),{path:i,remaining:a}=bindPath(n,s),p={method:n.method};let u=i;n.method==="GET"?u+=queryString(a):(p.body=JSON.stringify(a),p.headers={"content-type":"application/json",...n.idempotency==="not_required"?{}:{"idempotency-key":r["--idempotency-key"]??globalThis.crypto.randomUUID()}});const{response:c,text:l}=await request(e,u,p);if(!c.ok)throw apiError(c,l);print(formatBody(l))}function parseInput(e){if(e===void 0)return{};let r;try{r=JSON.parse(e)}catch(t){throw new UserError(`--input is not valid JSON: ${t.message}`)}if(r===null||typeof r!="object"||Array.isArray(r))throw new UserError("--input must be a JSON object");return r}function formatBody(e){if(e.trim()==="")return"";try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}}function print(e){process.stdout.write(`${e}
31
- `)}async function version(){const{readFileSync:e}=await import("node:fs");return JSON.parse(e(new URL("./package.json",import.meta.url),"utf8")).version}async function main(e,r){const{flags:t,positional:o}=parseArgv(e);if(t["--version"])return print(await version()),0;if(t["--help"]||o.length===0)return process.stdout.write(USAGE),o.length===0&&!t["--help"]?1:0;const[n,...s]=o,i=resolveConfig(t,r);if(n==="whoami")return await commandWhoami(i,t),0;if(n==="ops"){if(s[0]!=="list")throw new UserError('The only ops subcommand is "list"');return await commandOpsList(i,t,s[1]),0}if(n==="call")return await commandCall(i,t,s[0]),0;throw new UserError(`Unknown command: ${n}`)}main(process.argv.slice(2),process.env).then(e=>{process.exitCode=e},e=>{process.stderr.write(`${e instanceof UserError?e.message:e.stack??String(e)}
59
+ `;class c extends Error{}const ve=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(ve.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??v(Se(),".config","hyperscale")}function te(e){return v(ee(e),Ie)}function b(e,n){return v(ee(n),`session-${S(e)}.json`)}async function K(e,n){const t=b(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 U(v(J(t),s.name))}}async function Ue(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 H(e,n){const t=Ne(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 be(r,e),await j(e,384)}catch(o){throw await U(r),o}}function Ne(e){return J(e)}async function U(e){try{await $e(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 A(e){const n=te(e),t=await Ue(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 H(te(e),`${JSON.stringify(t,null,2)}
60
+ `)}function S(e){return me("sha256").update(`${e.baseUrl}\0${e.environment}\0${e.email}`).digest("hex")}async function q(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}
61
+ `)})}function oe(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 xe(e,n,t){await K(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 q(["-i"],s)).status===0?(await U(b(e,t)),"keychain"):(await H(b(e,t),`${JSON.stringify({account:r,credential:n})}
62
+ `),"file")}async function Le(e,n){const t=S(e);try{const r=JSON.parse(await _(b(e,n),"utf8"));if(!r||typeof r!="object"||Array.isArray(r)||r.account!==t)return{state:"invalid"};const o=oe(r.credential);return o?{credential:o,state:"valid"}:{state:"invalid"}}catch(r){return r&&r.code==="ENOENT"?{state:"missing"}:{state:"invalid"}}}async function D(e,n){await K(e,n);const t=await Le(e,n);if(t.state==="valid")return t.credential;if(t.state==="invalid")return null;const r=await q(["find-generic-password","-a",S(e),"-s",F,"-w"]);if(r.status===0){const o=oe(r.stdout.trim());if(o)return o}return null}async function M(e,n){await K(e,n);const t=S(e),r=await q(["delete-generic-password","-a",S(e),"-s",F]);return!r.unavailable&&r.status!==0&&r.status!==44?(await H(b(e,n),`${JSON.stringify({account:t,revoked:!0})}
63
+ `),{keychainDeleted:!1}):(await U(b(e,n)),{keychainDeleted:!0})}async function P(e,n){const t=await A(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 D(t,n);if(!o)throw new c('Session credential is missing. Run "hyperscale auth login".');return{...t,...o}}function re(e){return e.replace(/\p{Cc}/gu,n=>`
64
+ \r`.includes(n)?n:"")}function N(e){try{return new URL(e).origin}catch{return null}}function $(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=N(e.baseUrl);if(N(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:re(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 x(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 Y(e,n=!1){return process.stderr.write(e),process.stdin.isTTY&&typeof process.stdin.setRawMode=="function"?await Re(n):await Oe()}async function Oe(){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 Re(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(`
65
+ `),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===`
66
+ `){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=N(e.baseUrl);if(N(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:re(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 x(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 x(e,n,t=[]){let r=$(n.trim(),t);try{const o=JSON.parse(n);o&&o.error&&typeof o.error.message=="string"&&(r=$(`${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 Y("Email: ",!1)).trim();if(!r)throw new c("Login email is required");const o=await Y("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 Y("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.
67
+ `)}}}const i=_e(s,t.baseUrl),a=await A(n),d={email:r,...t,selectedProduct:a&&a.email===r&&a.baseUrl===t.baseUrl&&a.environment===t.environment?a.selectedProduct:null},l=await xe(d,{sessionToken:i.sessionToken,tenantId:i.tenantId},n);try{await ne(n,d)}catch(w){throw await M(d,n),w}l==="file"&&process.stderr.write(`macOS Keychain is unavailable. The session is in ${b(d,n)} with mode 0600.
68
+ `);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 A(n);if(!t){const s={authenticated:!1};e["--json"]?p(s):u("Not logged in.");return}const r=await D(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 A(n);if(!t){e["--json"]?p({status:"signed_out"}):u("Already logged out.");return}const r=await D(t,n);if(!r){const d=await M(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 x(o,s,[r.sessionToken]);const i=await M(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.
69
+ `)}function L(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}`:"",R=await y(e,`${n}${we}`,{},o,s);l===void 0&&(l=R),i.push(...L(R,r));const k=R.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 Ke(e,n){const t=await G(e,"/v1/blueprints",{},"blueprint.list"),r=L(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 He(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 qe(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 De(e){return new URLSearchParams({tenantId:e}).toString()}async function Me(e,n){const t=await P(e,n),r=await G(t,"/v1/products",{tenantId:t.tenantId},"product.list",t.sessionToken,[t.sessionToken]),o=L(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 Ye(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)}?${De(r.tenantId)}`,{},r.sessionToken,[r.sessionToken]);p(s)}function O(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 A(n);if(!t)throw new c("No Product selected. Log in and create or select a Product first.");const r=O(t,e),o=T(e,n,t);if(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){const t=await ce(e,n),r=`/v1/workspaces/${encodeURIComponent(t.workspaceId)}/operation-set`,{response:o,text:s}=await C(t,r);if(!o.ok)throw de(o,s,[t.apiKey]);const i=ze(s,o.status);if(e["--json"]){p(i);return}if(i.status==="legacy"){u("This Product uses a legacy surface. It has no frozen public verbs yet."),u(`Build: ${i.workspaceBuildId}`);return}if(i.publicVerbs.length===0)u("No public verbs");else{u("Verb Noun Input Kind");for(const a of i.publicVerbs){const d=a.idempotencyPolicy?.mode==="not_required"?"read-only":"mutation";u(`${a.name} ${a.noun} ${Ge(a.inputSchema)} ${d}`)}}u(`Digest: ${i.tenantOperationSetDigest}`),u(`Build: ${i.workspaceBuildId}`),u(`Surface version: ${i.tenantSurfaceVersion}`)}function ze(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 Ge(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 We(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:Qe(i)});return}u(`Operation: ${a??"not returned"}`),u(`Receipt: ${d??"not returned"}`);const l=fe(i);l&&u(l)}function Qe(e){if(e.trim()==="")return null;try{return JSON.parse(e)}catch{return e}}function de(e,n,t=[]){const r=$(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=$(`HTTP ${e.status} \xB7 ${s}${o.error.message}`,t),a=Ze(o.error.details,t);return new c([i,...a].join(`
70
+ `))}function Ze(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}: ${$(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)}: ${$(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)}: ${$(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 Xe(e,n){const t=await P(e,n),r=O(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 et(e,n){const t=await P(e,n),r=O(t,e),o=await G(t,"/v1/api-keys",{tenantId:t.tenantId},"api_key.list",t.sessionToken,[t.sessionToken]),s=L(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 tt(e,n,t){if(!t)throw new c("key revoke needs an API key id");const r=await P(e,n),o=O(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 nt(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 rt(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 st(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 it(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}=rt(o,s),d={method:o.method};let l=i;o.method==="GET"?l+=st(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 x(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}
71
+ `)}function p(e){u(JSON.stringify(e,null,2))}async function at(){const{readFileSync:e}=await import("node:fs");return JSON.parse(e(new URL("./package.json",import.meta.url),"utf8")).version}async function ct(e,n){const{flags:t,positional:r}=Ee(e);if(t["--version"])return u(await at()),0;if(t["--help"]||r.length===0)return process.stdout.write(Ae),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 Ke(a,t);else if(s[0]==="show")await He(a,t,s[1]);else throw new c('Blueprint subcommands are "list" and "show"');return 0}if(o==="product"){if(s[0]==="create")await qe(t,n);else if(s[0]==="list")await Me(t,n);else if(s[0]==="show")await Ye(t,n,s[1]);else if(s[0]==="verbs")await Ve(t,n);else if(s[0]==="run")await We(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 Xe(t,n);else if(s[0]==="list")await et(t,n);else if(s[0]==="revoke")await tt(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 nt(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 it(i,t,s[0]),0;throw new c(`Unknown command: ${o}`)}ct(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
72
  `),process.exitCode=1});
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hyperscale0/cli",
3
- "version": "1.0.0-alpha.3",
4
- "description": "The Hyperscale command line: drive one Product's operation surface with a Product API key.",
3
+ "version": "1.0.0-alpha.5",
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": [