infernoflow 0.39.0 → 0.40.0
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog — infernoflow
|
|
2
2
|
|
|
3
|
+
## 0.40.0 — 2026-05-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Experimental Supabase JWT auth (`infernoflow login --browser`)** — opens your browser to Supabase's GitHub OAuth, captures the session via a one-shot localhost callback (port range 47655–47659), and stores the JWT + refresh token. After this, cloud writes are authenticated under your `auth.uid()` and the per-user RLS policy `auth.uid() = user_id` becomes enforceable. Opt-in until end-to-end verified against a real Supabase project.
|
|
7
|
+
- **Automatic token refresh** — `pushEntry` calls `refreshSessionIfNeeded()` before each write, hitting `/auth/v1/token?grant_type=refresh_token` within 5 minutes of expiry. Falls back silently to anon-key dev mode if refresh fails so local logging is never blocked.
|
|
8
|
+
- **Tagged credentials schema** — `mode: "supabase" | "device-flow"` with full backward-compatible reads of pre-v0.40 single-`access_token` files. New `getSupabaseAccessToken()` helper for synchronous JWT lookup with expiry awareness.
|
|
9
|
+
- **`doctor` full credential-state recognition** — distinct messages for not-logged-in, supabase-authenticated, identity-only device-flow, and legacy schema (with re-login nudge).
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **Default `infernoflow login` is unchanged** — still GitHub Device Flow, still works exactly as it did in v0.38–0.39. The new browser-OAuth path is opt-in via `--browser` until the Supabase project setup (allow-list URLs, GitHub provider, schema apply) is confirmed working end-to-end.
|
|
13
|
+
- **`scripts/supabase-schema.sql`** — `user_id` now defaults to `auth.uid()` so authenticated writes (when you opt in) auto-populate it. Schema is fully idempotent. Both RLS policies retained: "Users own their entries" enforces the authenticated path; "Anon can insert (dev mode)" keeps the device-flow path working.
|
|
14
|
+
- **`whoami`** prints the auth mode (Supabase JWT vs identity-only) and JWT expiry when present.
|
|
15
|
+
|
|
16
|
+
### Required setup before `--browser` works
|
|
17
|
+
On your Supabase project, one-time:
|
|
18
|
+
1. Authentication → Providers → enable GitHub.
|
|
19
|
+
2. Authentication → URL Configuration → Redirect URLs: add `http://localhost:47655/callback` through `http://localhost:47659/callback`.
|
|
20
|
+
3. SQL Editor → paste and run `scripts/supabase-schema.sql` (idempotent).
|
|
21
|
+
|
|
22
|
+
Then: `infernoflow logout && infernoflow login --browser`. If anything misbehaves, plain `infernoflow login` still works.
|
|
23
|
+
|
|
3
24
|
## 0.39.0 — 2026-05-02
|
|
4
25
|
|
|
5
26
|
### Added
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
`,{mode:384})}function
|
|
1
|
+
import*as r from"node:fs";import*as i from"node:path";import*as c from"node:os";const n=i.join(c.homedir(),".infernoflow"),t=i.join(n,"credentials.json");function o(){try{return r.existsSync(t)?JSON.parse(r.readFileSync(t,"utf8")):null}catch{return null}}function u(e){r.existsSync(n)||r.mkdirSync(n,{recursive:!0}),r.writeFileSync(t,JSON.stringify(e,null,2)+`
|
|
2
|
+
`,{mode:384})}function a(){try{return r.existsSync(t)&&r.unlinkSync(t),!0}catch{return!1}}function f(){const e=o();return e?!!(e.mode==="supabase"&&e.access_token||e.mode==="device-flow"&&e.github_access_token||!e.mode&&e.access_token):!1}function l(){const e=o();if(!e||e.mode!=="supabase"||!e.access_token)return null;if(e.expires_at){const s=Date.parse(e.expires_at);if(!Number.isNaN(s)&&Date.now()>s-6e4)return null}return e.access_token}export{a as deleteCredentials,l as getSupabaseAccessToken,f as isLoggedIn,o as readCredentials,u as writeCredentials};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as A from"node:https";import{readCredentials as S,writeCredentials as w,getSupabaseAccessToken as d}from"./credentials.mjs";const o=process.env.INFERNOFLOW_SUPABASE_URL||"https://vscesbbtmrsctfroigyx.supabase.co",i=process.env.INFERNOFLOW_SUPABASE_ANON_KEY||"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZzY2VzYmJ0bXJzY3Rmcm9pZ3l4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc0ODAxMjcsImV4cCI6MjA5MzA1NjEyN30.4WCXr0aGBlqC2m29DnlCSu5qKl0L-fDQoaV9AGu8-68";function c(e,r,s,t){return new Promise((n,h)=>{const f=new URL(r),u=s?JSON.stringify(s):null,_={hostname:f.hostname,port:443,path:f.pathname+f.search,method:e,headers:{"Content-Type":"application/json",Accept:"application/json","User-Agent":"infernoflow-cli",apikey:i,...t,...u?{"Content-Length":Buffer.byteLength(u)}:{}}},a=A.request(_,p=>{let l="";p.on("data",y=>l+=y),p.on("end",()=>{try{n({status:p.statusCode,body:JSON.parse(l)})}catch{n({status:p.statusCode,body:l})}})});a.on("error",h),a.setTimeout(8e3,()=>{a.destroy(new Error("timeout"))}),u&&a.write(u),a.end()})}async function m(){const e=S();if(!e||e.mode!=="supabase"||!e.refresh_token)return;const r=e.expires_at?Date.parse(e.expires_at):0,s=300*1e3;if(!(Number.isFinite(r)&&Date.now()<r-s))try{const t=await c("POST",`${o}/auth/v1/token?grant_type=refresh_token`,{refresh_token:e.refresh_token},{Authorization:`Bearer ${i}`});if(t.status!==200||!t.body?.access_token)return;const n={...e,access_token:t.body.access_token,refresh_token:t.body.refresh_token||e.refresh_token,expires_at:t.body.expires_in?new Date(Date.now()+t.body.expires_in*1e3).toISOString():e.expires_at};w(n)}catch{}}async function x(e,r,s){try{await m();const t=d(),n=!!t,h={project_id:s,ts:e.ts,type:e.type||"note",summary:e.summary,result:e.result||null,source:e.source||null,auto:e.auto||!1,agent:e.agent||null,...n?{}:{user_token:r}};await c("POST",`${o}/rest/v1/entries`,h,{Authorization:`Bearer ${n?t:i}`,apikey:i,Prefer:"return=minimal"})}catch{}}async function I(e){const r=await c("GET",`${o}/auth/v1/user`,null,{Authorization:`Bearer ${e}`});return r.status===200?r.body:null}async function k(e){await m();const r=d(),s=new URLSearchParams({project_id:`eq.${e}`,order:"ts.asc",limit:"10000"}),t=await c("GET",`${o}/rest/v1/entries?${s.toString()}`,null,{Authorization:`Bearer ${r||i}`});return t.status===200&&Array.isArray(t.body)?t.body:[]}async function g(e){return await c("POST",`${o}/auth/v1/token?grant_type=pkce`,{auth_code:e},{})}function O(e,r){const s=new URLSearchParams({provider:"github",redirect_to:r,state:e});return`${o}/auth/v1/authorize?${s.toString()}`}export{i as SUPABASE_ANON_KEY,o as SUPABASE_URL,g as exchangeCodeForSession,O as getOAuthUrl,I as getUser,k as pullEntries,x as pushEntry,m as refreshSessionIfNeeded};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import*as s from"node:fs";import*as l from"node:path";import*as h from"node:os";import*as O from"node:http";import{execSync as b,spawnSync as k}from"node:child_process";import{fileURLToPath as v}from"node:url";import{bold as C,cyan as N,gray as w,green as y,yellow as x,red as j}from"../ui/output.mjs";import{detectAvailableProviders as A}from"../ai/providerRouter.mjs";function
|
|
2
|
-
`).filter(Boolean).length}catch{}return
|
|
1
|
+
import*as s from"node:fs";import*as l from"node:path";import*as h from"node:os";import*as O from"node:http";import{execSync as b,spawnSync as k}from"node:child_process";import{fileURLToPath as v}from"node:url";import{bold as C,cyan as N,gray as w,green as y,yellow as x,red as j}from"../ui/output.mjs";import{detectAvailableProviders as A}from"../ai/providerRouter.mjs";function c(n,o){try{const e=o();return{label:n,...e}}catch(e){return{label:n,status:"error",message:e.message,fix:null}}}function a(n,o){return{status:"pass",message:n,detail:o||null,fix:null}}function f(n,o){return{status:"warn",message:n,detail:null,fix:o||null}}function m(n,o){return{status:"fail",message:n,detail:null,fix:o||null}}function _(){const n=process.version,o=parseInt(n.slice(1).split(".")[0],10);return o>=20?a(`Node.js ${n}`,"Node 20+ recommended"):o>=18?a(`Node.js ${n}`):m(`Node.js ${n} \u2014 infernoflow requires Node 18+`,"Install Node 20 from nodejs.org")}function P(){try{const n=k("infernoflow",["--version"],{encoding:"utf8",timeout:5e3});return n.status===0?a(`infernoflow v${n.stdout.trim()} installed`):m("infernoflow CLI not found on PATH","npm install -g infernoflow")}catch{return m("infernoflow CLI not found on PATH","npm install -g infernoflow")}}function E(n){try{return b("git rev-parse --git-dir",{cwd:n,stdio:"ignore"}),a("Git repository detected")}catch{return m("Not a git repository","git init && git add . && git commit -m 'init'")}}function I(n){const o=l.join(n,"inferno");return s.existsSync(o)?a("inferno/ directory exists"):m("inferno/ not found","infernoflow init")}function T(n){const o=l.join(n,"inferno");if((()=>{try{return JSON.parse(s.readFileSync(l.join(o,"config.json"),"utf8"))}catch{return{}}})().mode==="memory"){const i=l.join(o,"sessions.jsonl");if(!s.existsSync(i))return a("Memory mode \u2014 sessions.jsonl will be created on first log");let t=0;try{t=s.readFileSync(i,"utf8").split(`
|
|
2
|
+
`).filter(Boolean).length}catch{}return a(`Memory mode \u2014 ${t} session entr${t===1?"y":"ies"}`)}for(const i of["contract.json","capabilities.json"]){const t=l.join(o,i);if(s.existsSync(t))try{const u=(JSON.parse(s.readFileSync(t,"utf8")).capabilities||[]).length;return a(`${i} valid \u2014 ${u} capabilities`)}catch{return m(`${i} contains invalid JSON`,`Fix the JSON syntax in inferno/${i}`)}}return m("No contract.json/capabilities.json (and not in memory mode)","infernoflow init or infernoflow init --mode full")}function S(n){try{return JSON.parse(s.readFileSync(l.join(n,"inferno","config.json"),"utf8")).mode==="memory"}catch{return!1}}function G(n){if(S(n))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const o=l.join(n,"inferno","scenarios");if(!s.existsSync(o))return f("No scenarios/ directory","infernoflow init");const e=s.readdirSync(o).filter(i=>i.endsWith(".json"));return e.length?a(`${e.length} scenario file${e.length!==1?"s":""} found`):f("scenarios/ is empty","Add scenario files or run infernoflow suggest")}function F(n){if(S(n))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const o=l.join(n,"inferno","CHANGELOG.md");return s.existsSync(o)?a("inferno/CHANGELOG.md exists"):f("No inferno/CHANGELOG.md","infernoflow init")}function M(n){if(S(n))return{status:"info",message:"n/a in memory mode (CLAUDE.md is auto-maintained)",detail:null,fix:null};const o=l.join(n,"inferno","CONTEXT.md");if(!s.existsSync(o))return f("No CONTEXT.md generated","infernoflow context");const e=(Date.now()-s.statSync(o).mtimeMs)/(1e3*60*60*24);return e>7?f(`CONTEXT.md is ${Math.round(e)} days old \u2014 may be stale`,"infernoflow context"):a(`CONTEXT.md present (${Math.round(e)}d old)`)}function D(n){const o=l.join(n,".git","hooks"),e=l.join(o,"post-commit"),i=l.join(o,"pre-push"),t=s.existsSync(e)&&s.readFileSync(e,"utf8").includes("infernoflow"),r=s.existsSync(i)&&s.readFileSync(i,"utf8").includes("infernoflow");return t&&r?a("Git hooks installed (post-commit + pre-push)"):f(t||r?"Partial git hooks installed":"Git hooks not installed","infernoflow setup --yes")}function L(n){const o=[l.join(n,".cursor","mcp.json"),l.join(n,".mcp.json"),l.join(h.homedir(),".cursor","mcp.json"),l.join(h.homedir(),"Library","Application Support","Claude","claude_desktop_config.json"),l.join(h.homedir(),"AppData","Roaming","Claude","claude_desktop_config.json")];for(const e of o)if(s.existsSync(e))try{const i=JSON.parse(s.readFileSync(e,"utf8")),t=i.mcpServers||i.mcp_servers||{};if(Object.keys(t).some(r=>r.toLowerCase().includes("inferno")))return a(`MCP server configured in ${l.basename(e)}`)}catch{}return f("MCP server not configured","infernoflow setup --yes (adds to Cursor/Claude config)")}function R(n){const o=A(n),e=Object.entries(o).filter(([,i])=>i).map(([i])=>i);return e.length?a(`AI provider${e.length!==1?"s":""}: ${e.join(", ")}`):f("No AI provider configured",`Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY, or OPENROUTER_API_KEY
|
|
3
3
|
Or install Ollama (ollama.com) for free local AI
|
|
4
|
-
Or use VS Code with GitHub Copilot (zero config)`)}async function
|
|
4
|
+
Or use VS Code with GitHub Copilot (zero config)`)}async function J(){return new Promise(n=>{const o=O.get({hostname:"localhost",port:11434,path:"/api/tags",timeout:1500},e=>{n(a("Ollama running on localhost:11434"))});o.on("error",()=>n({status:"info",message:"Ollama not running (optional)",fix:"ollama serve",detail:null})),o.on("timeout",()=>{o.destroy(),n({status:"info",message:"Ollama not running (optional)",fix:null,detail:null})})})}function H(){const n=l.join(h.homedir(),".infernoflow","credentials.json");if(!s.existsSync(n))return{status:"info",message:"Not logged in to cloud (optional)",fix:"infernoflow login",detail:null};try{const o=JSON.parse(s.readFileSync(n,"utf8")),e=o.user?.login||o.user?.name||o.user?.email||"unknown";if(o.mode==="supabase"&&o.access_token){if(o.expires_at){const i=new Date(o.expires_at).getTime();if(Date.now()>i)return f(`JWT expired for ${e} \u2014 refresh on next log will retry`,"infernoflow login")}return a(`Authenticated as ${e} (Supabase JWT \u2014 auth.uid() writes)`)}return o.mode==="device-flow"&&o.github_access_token?{status:"info",message:`Identity-only as ${e} (device flow \u2014 anon-mode writes)`,fix:"infernoflow login (without --device-flow, for full auth)",detail:null}:o.access_token?f(`Legacy login for ${e} \u2014 re-run for authenticated cloud writes`,"infernoflow logout && infernoflow login"):{status:"info",message:"Credentials file present but no recognised token",fix:"infernoflow logout && infernoflow login",detail:null}}catch{return f("Credentials file unreadable","infernoflow logout && infernoflow login")}}function X(){try{const n=v(import.meta.url),o=l.resolve(l.dirname(n),"..","..","bin","infernoflow.mjs");if(!s.existsSync(o))return{status:"info",message:"bin/infernoflow.mjs not found from doctor location",fix:null,detail:null};const i=[...s.readFileSync(o,"utf8").matchAll(/import\("\.\.\/lib\/(commands\/[^"]+|telemetry\.mjs)"\)/g)],t=[],r=l.resolve(l.dirname(o),"..");for(const u of i){const g=u[1],$=l.join(r,"lib",g);s.existsSync($)||t.push(g)}return t.length?m(`${t.length} routed command(s) missing module files: ${t.slice(0,3).join(", ")}${t.length>3?"\u2026":""}`,"Restore the missing files or remove their entries from bin/infernoflow.mjs"):a(`All ${i.length} routed commands resolve to real files`)}catch(n){return{status:"info",message:`Router integrity check skipped: ${n.message}`,fix:null,detail:null}}}function K(n){const o=l.join(n,".gitignore");if(!s.existsSync(o))return{status:"info",message:".gitignore not found",fix:null,detail:null};const e=s.readFileSync(o,"utf8");return/^(?:\*\*\/)?node_modules\/?$/m.test(e)?a(".gitignore excludes node_modules"):f(".gitignore does not exclude node_modules","Add 'node_modules/' (and '**/node_modules/') to .gitignore")}function W(n,o){const e=n.filter(t=>t.status==="warn"&&t.fix),i=[];for(const t of e){const r=t.fix;if(r.startsWith("infernoflow ")){const u=r.slice(12).split(" ");k("infernoflow",u,{cwd:o,encoding:"utf8",timeout:3e4}).status===0&&i.push(t.label)}}return i}function Y(n){return n==="pass"?y("\u2714"):n==="warn"?x("\u26A0"):n==="fail"?j("\u2717"):w("\xB7")}function U(n,o){const e={pass:0,warn:0,fail:0,info:0,error:0};for(const r of n)e[r.status]=(e[r.status]||0)+1;console.log(),console.log(` ${C("\u{1F525} infernoflow doctor")}`),console.log();const i=Math.max(...n.map(r=>r.label.length))+2;for(const r of n)console.log(` ${Y(r.status)} ${C(r.label.padEnd(i))} ${r.message}`),r.detail&&console.log(` ${" ".repeat(i)} ${w(r.detail)}`),r.fix&&(r.status==="warn"||r.status==="fail")&&console.log(` ${" ".repeat(i)} ${N("fix:")} ${w(r.fix)}`);console.log();const t=e.fail>0?j("issues found"):e.warn>0?x("warnings"):y("all good");console.log(` ${t} \u2014 ${y(String(e.pass))} pass \xB7 ${x(String(e.warn))} warn \xB7 ${j(String(e.fail))} fail (${o}ms)`),console.log(),(e.warn>0||e.fail>0)&&(console.log(` Run ${N("infernoflow doctor --fix")} to auto-fix warnings`),console.log())}async function q(n){const o=n.slice(1),e=o.includes("--json"),i=o.includes("--fix"),t=process.cwd(),r=Date.now(),u=[c("Node.js version",()=>_()),c("infernoflow CLI",()=>P()),c("Git repository",()=>E(t)),c("inferno/ directory",()=>I(t)),c("Contract / mode",()=>T(t)),c("Scenarios",()=>G(t)),c("Changelog",()=>F(t)),c("CONTEXT.md",()=>M(t)),c("Git hooks",()=>D(t)),c("MCP server",()=>L(t)),c("AI providers",()=>R(t)),c("Cloud sync",()=>H()),c(".gitignore",()=>K(t)),c("Router integrity",()=>X()),await J().then(d=>({label:"Ollama (local AI)",...d}))],g=Date.now()-r;if(i){const d=W(u,t);if(d.length)return e||(console.log(),d.forEach(p=>console.log(` ${y("\u2714")} Fixed: ${p}`)),console.log()),q(["doctor","--json"])}if(e){const d={pass:0,warn:0,fail:0,info:0};u.forEach(p=>d[p.status]=(d[p.status]||0)+1),console.log(JSON.stringify({ok:d.fail===0,counts:d,results:u,elapsed:g}));return}U(u,g),u.some(d=>d.status==="fail")&&process.exit(1)}export{q as doctorCommand};
|
|
@@ -1 +1,35 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as $ from"node:https";import*as k from"node:http";import*as O from"node:crypto";import{execSync as R}from"node:child_process";import{bold as p,cyan as m,gray as a,green as _,red as v,yellow as x}from"../ui/output.mjs";import{readCredentials as L,writeCredentials as A,deleteCredentials as M,isLoggedIn as G}from"../cloud/credentials.mjs";import{SUPABASE_URL as N,getUser as B}from"../cloud/supabase.mjs";const T="Ov23liYuUKwDRTzrywsa",J=[47655,47656,47657,47658,47659],z=300*1e3;function S(e,s,d){return new Promise((t,l)=>{const o=new URLSearchParams(d).toString(),n={hostname:e,port:443,path:s,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json","User-Agent":"infernoflow-cli","Content-Length":Buffer.byteLength(o)}},i=$.request(n,r=>{let c="";r.on("data",u=>c+=u),r.on("end",()=>{try{t(JSON.parse(c))}catch{t(c)}})});i.on("error",l),i.setTimeout(15e3,()=>i.destroy(new Error("timeout"))),i.write(o),i.end()})}function W(e,s,d){return new Promise((t,l)=>{const o={hostname:e,port:443,path:s,method:"GET",headers:{Accept:"application/json","User-Agent":"infernoflow-cli",Authorization:`Bearer ${d}`}},n=$.request(o,i=>{let r="";i.on("data",c=>r+=c),i.on("end",()=>{try{t(JSON.parse(r))}catch{t(r)}})});n.on("error",l),n.setTimeout(1e4,()=>n.destroy(new Error("timeout"))),n.end()})}function C(e){return new Promise(s=>setTimeout(s,e))}function E(e){try{const s=process.platform==="win32"?`start "" "${e}"`:process.platform==="darwin"?`open "${e}"`:`xdg-open "${e}"`;R(s,{stdio:"ignore"})}catch{}}function F(e){return new Promise((s,d)=>{let t=0;const l=()=>{if(t>=e.length)return d(new Error("no available local port for callback"));const o=e[t++],n=k.createServer();n.on("error",()=>l()),n.listen(o,"127.0.0.1",()=>{n.close(()=>s(o))})};l()})}const Y=`<!DOCTYPE html>
|
|
2
|
+
<html><head><meta charset="utf-8"><title>infernoflow login</title>
|
|
3
|
+
<style>
|
|
4
|
+
body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; max-width: 480px; margin: 60px auto; padding: 0 24px; color: #0f1117; }
|
|
5
|
+
.ok { color: #16a34a; }
|
|
6
|
+
.err { color: #dc2626; }
|
|
7
|
+
code { background: #f4f4f5; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
|
|
8
|
+
</style></head>
|
|
9
|
+
<body>
|
|
10
|
+
<h2>\u{1F525} infernoflow</h2>
|
|
11
|
+
<p id="status">Completing login\u2026</p>
|
|
12
|
+
<script>
|
|
13
|
+
(async () => {
|
|
14
|
+
const status = document.getElementById('status');
|
|
15
|
+
const hash = window.location.hash.substring(1);
|
|
16
|
+
if (!hash) {
|
|
17
|
+
const params = new URLSearchParams(window.location.search);
|
|
18
|
+
const errMsg = params.get('error_description') || params.get('error') || 'No tokens received.';
|
|
19
|
+
status.innerHTML = '<span class="err">\u2718 Login failed: ' + errMsg + '</span>';
|
|
20
|
+
try { await fetch('/error?msg=' + encodeURIComponent(errMsg)); } catch (_) {}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const r = await fetch('/token', { method: 'POST', body: hash, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
|
|
25
|
+
if (r.ok) {
|
|
26
|
+
status.innerHTML = '<span class="ok">\u2714 Logged in.</span> You can close this tab and return to your terminal.';
|
|
27
|
+
} else {
|
|
28
|
+
status.innerHTML = '<span class="err">\u2718 Forwarding failed (HTTP ' + r.status + ').</span>';
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {
|
|
31
|
+
status.innerHTML = '<span class="err">\u2718 Could not reach the local CLI: ' + e.message + '</span>';
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
</script>
|
|
35
|
+
</body></html>`;async function K(){const e=await F(J),s=`http://localhost:${e}/callback`,d=O.randomBytes(16).toString("hex"),t=new URL(`${N}/auth/v1/authorize`);return t.searchParams.set("provider","github"),t.searchParams.set("redirect_to",s),t.searchParams.set("scopes","read:user user:email"),new Promise((l,o)=>{let n=!1,i;const r=u=>{if(!n){n=!0,clearTimeout(c);try{i?.close()}catch{}u()}},c=setTimeout(()=>{r(()=>o(new Error("login timed out \u2014 close the browser tab and try again")))},z);i=k.createServer((u,g)=>{const w=new URL(u.url,`http://localhost:${e}`);if(u.method==="GET"&&w.pathname==="/callback"){g.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),g.end(Y);return}if(u.method==="POST"&&w.pathname==="/token"){let h="";u.on("data",f=>h+=f),u.on("end",()=>{const f=new URLSearchParams(h),y=f.get("access_token"),I=f.get("refresh_token"),b=parseInt(f.get("expires_in")||"0",10),D=f.get("provider_token"),H=f.get("provider_refresh_token");if(!y){g.writeHead(400,{"Content-Type":"application/json"}),g.end(JSON.stringify({error:"no access_token in fragment"}));return}g.writeHead(204),g.end(),r(()=>l({access_token:y,refresh_token:I,expires_at:b?new Date(Date.now()+b*1e3).toISOString():null,provider_token:D,provider_refresh_token:H}))});return}if(u.method==="GET"&&w.pathname==="/error"){const h=w.searchParams.get("msg")||"unknown error";g.writeHead(204),g.end(),r(()=>o(new Error(h)));return}g.writeHead(404),g.end()}),i.on("error",u=>r(()=>o(u))),i.listen(e,"127.0.0.1",()=>{console.log(),console.log(` ${p("\u{1F525} infernoflow login")}`),console.log(),console.log(` ${a("Opening your browser to sign in with GitHub via Supabase\u2026")}`),console.log(),console.log(` ${p("If the browser doesn't open, paste this URL:")}`),console.log(` ${m(t.toString())}`),console.log(),console.log(` ${a(`Listening for the callback on http://localhost:${e}/callback`)}`),console.log(` ${a("(this prompt will close automatically when you finish)")}`),console.log(),E(t.toString())})})}async function j(){console.log(),console.log(` ${p("\u{1F525} infernoflow login")} ${a("(device-flow / identity-only)")}`),console.log(),console.log(` ${x("\u26A0")} ${a("Device flow gives us your GitHub identity but no Supabase JWT.")}`),console.log(` ${a("Cloud writes will fall back to anon-key dev mode. Run without --device-flow")}`),console.log(` ${a("for the proper authenticated flow.")}`),console.log();let e;try{e=await S("github.com","/login/device/code",{client_id:T,scope:"read:user user:email"})}catch(r){throw new Error(`could not reach GitHub: ${r.message}`)}if(!e.device_code)throw new Error(`GitHub error: ${JSON.stringify(e)}`);const{device_code:s,user_code:d,verification_uri:t,expires_in:l,interval:o}=e,n=(o||5)*1e3;console.log(` ${p("Open:")} ${m(t)}`),console.log(` ${p("Code:")} ${p(m(d))}`),console.log(),E(t),console.log(` ${a("Waiting for you to authorize\u2026")} ${a("(Ctrl+C to cancel)")}`),console.log();const i=Date.now()+(l||900)*1e3;for(;Date.now()<i;){await C(n);let r;try{r=await S("github.com","/login/oauth/access_token",{client_id:T,device_code:s,grant_type:"urn:ietf:params:oauth:grant-type:device_code"})}catch{continue}if(r.error!=="authorization_pending"){if(r.error==="slow_down"){await C(5e3);continue}if(r.error==="expired_token")throw new Error("code expired \u2014 run infernoflow login again");if(r.error==="access_denied")throw new Error("access denied");if(r.access_token){const c=await W("api.github.com","/user",r.access_token);return{mode:"device-flow",github_access_token:r.access_token,user:{provider:"github",login:c?.login||null,name:c?.name||null,email:c?.email||null,id:c?.id||null,avatar_url:c?.avatar_url||null}}}}}throw new Error("login timed out")}async function Q(e){if(G()){const o=L(),n=o?.user?.login||o?.user?.name||o?.user?.email||"unknown";console.log(),console.log(` ${_("\u2714")} Already logged in as ${p(n)}`),console.log(` Run ${m("infernoflow logout")} to sign out.`),console.log();return}const s=e.includes("--browser"),d=!s;let t;try{if(d){const o=await j();t={mode:"device-flow",github_access_token:o.github_access_token,user:o.user,logged_in_at:new Date().toISOString()}}else{const o=await K(),n=await B(o.access_token).catch(()=>null);t={mode:"supabase",access_token:o.access_token,refresh_token:o.refresh_token,expires_at:o.expires_at,provider_token:o.provider_token,provider_refresh_token:o.provider_refresh_token,user:n?{provider:"github",id:n.id||null,email:n.email||null,login:n.user_metadata?.user_name||n.user_metadata?.preferred_username||n.identities?.[0]?.identity_data?.user_name||null,name:n.user_metadata?.full_name||n.user_metadata?.name||null,avatar_url:n.user_metadata?.avatar_url||null}:{provider:"github"},logged_in_at:new Date().toISOString()}}}catch(o){console.log(),console.log(` ${v("\u2718")} Login failed: ${o.message}`),console.log(s?` ${a("If --browser fails, fall back to the default flow:")} ${m("infernoflow login")}`:` ${a("To try the experimental authenticated browser flow:")} ${m("infernoflow login --browser")}`),console.log(),process.exit(1)}A(t);const l=t.user?.login||t.user?.name||t.user?.email||"unknown";console.log(),console.log(` ${_("\u2714")} Logged in as ${p(l)}`),console.log(),t.mode==="supabase"?console.log(` ${a("Cloud sync is now authenticated. Every")} ${m("infernoflow log")} ${a("writes under your auth.uid().")}`):console.log(` ${a("Identity-only login (device flow). Cloud writes still use the anon-key dev mode.")}`),console.log()}function P(){const e=M();console.log(),console.log(e?` ${_("\u2714")} Logged out. Local credentials removed.`:` ${a("Already logged out.")}`),console.log()}function U(){const e=L();if(console.log(),!e?.access_token&&!e?.github_access_token){console.log(` ${a("Not logged in.")} Run ${m("infernoflow login")}`),console.log();return}const s=e.user?.login||e.user?.name||e.user?.email||"unknown",d=e.user?.email||a("(no email)"),t=e.logged_in_at?new Date(e.logged_in_at).toLocaleDateString():"unknown",l=e.mode==="supabase"?_("\u2714 authenticated (Supabase JWT)"):e.mode==="device-flow"?x("\u26A0 identity-only (device flow)"):a("legacy");if(console.log(` ${p("\u{1F525} infernoflow")} \u2014 logged in as:`),console.log(),console.log(` User: ${p(s)}`),console.log(` Email: ${d}`),console.log(` Since: ${a(t)}`),console.log(` Mode: ${l}`),e.expires_at){const o=new Date(e.expires_at),n=Date.now()>o.getTime();console.log(` Expires: ${a(o.toLocaleString())}${n?" "+v("(expired \u2014 run login again)"):""}`)}console.log()}async function ee(e){const s=e[1];return s==="logout"?P():s==="whoami"?U():Q(e)}async function oe(){return P()}async function ne(){return U()}export{ee as loginCommand,oe as logoutCommand,ne as whoamiCommand};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infernoflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Persistent memory for AI coding sessions \u2014 captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|