@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.
Files changed (3) hide show
  1. package/README.md +12 -4
  2. package/hyperscale.js +46 -11
  3. 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 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 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
- 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>
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 DEFAULT_BASE_URL="https://hyperscale0.ai",DEFAULT_ENVIRONMENT="sandbox",REQUEST_TIMEOUT_MS=3e4,USAGE=`hyperscale <command> [options]
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
- 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
+ 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 ${DEFAULT_BASE_URL})
12
- --environment <name> sandbox or live (default: $HYPERSCALE_ENVIRONMENT, else ${DEFAULT_ENVIRONMENT})
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
- The key rides in the environment, never on the command line, unless you are
20
- pasting a one-off into a throwaway shell.
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 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)}
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.3",
4
- "description": "The Hyperscale command line: drive one Product's operation surface with a Product API key.",
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": [