@lunora/cli 1.0.0-alpha.454 → 1.0.0-alpha.456
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/dist/packem_chunks/cli.mjs +2 -2
- package/dist/packem_chunks/handler.mjs +2 -2
- package/dist/packem_chunks/handler26.mjs +99 -1
- package/dist/packem_chunks/handler27.mjs +15 -99
- package/dist/packem_chunks/handler28.mjs +1 -15
- package/dist/packem_chunks/handler29.mjs +1 -1
- package/dist/packem_shared/{runDeployCommand-C5L6ZhGV.mjs → runDeployCommand-Tgy9tGRV.mjs} +1 -1
- package/dist/packem_shared/vectorize-metadata-BVSF8z2h.mjs +2 -0
- package/package.json +10 -10
- package/skills/lunora-cloudflare-audit/SKILL.md +1 -0
- package/skills/lunora-deploy/SKILL.md +3 -0
- package/skills/lunora-setup-mail/SKILL.md +3 -1
- package/dist/packem_shared/vectorize-metadata-DOB1akh6.mjs +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/cli",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.456",
|
|
4
4
|
"description": "The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
@@ -56,16 +56,16 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@bomb.sh/tab": "0.0.22",
|
|
59
|
-
"@lunora/advisor": "1.0.0-alpha.
|
|
60
|
-
"@lunora/bindings": "1.0.0-alpha.
|
|
61
|
-
"@lunora/codegen": "1.0.0-alpha.
|
|
62
|
-
"@lunora/config": "1.0.0-alpha.
|
|
63
|
-
"@lunora/container": "1.0.0-alpha.
|
|
64
|
-
"@lunora/d1": "1.0.0-alpha.
|
|
59
|
+
"@lunora/advisor": "1.0.0-alpha.274",
|
|
60
|
+
"@lunora/bindings": "1.0.0-alpha.142",
|
|
61
|
+
"@lunora/codegen": "1.0.0-alpha.376",
|
|
62
|
+
"@lunora/config": "1.0.0-alpha.431",
|
|
63
|
+
"@lunora/container": "1.0.0-alpha.100",
|
|
64
|
+
"@lunora/d1": "1.0.0-alpha.222",
|
|
65
65
|
"@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
|
|
66
|
-
"@lunora/mcp": "1.0.0-alpha.
|
|
67
|
-
"@lunora/runtime": "1.0.0-alpha.
|
|
68
|
-
"@lunora/seed": "1.0.0-alpha.
|
|
66
|
+
"@lunora/mcp": "1.0.0-alpha.277",
|
|
67
|
+
"@lunora/runtime": "1.0.0-alpha.258",
|
|
68
|
+
"@lunora/seed": "1.0.0-alpha.270",
|
|
69
69
|
"@visulima/cerebro": "4.0.1",
|
|
70
70
|
"@visulima/error": "6.1.0",
|
|
71
71
|
"@visulima/fs": "7.0.0",
|
|
@@ -47,6 +47,7 @@ alerts setup` from an audit. Show the change and get explicit approval first.
|
|
|
47
47
|
|
|
48
48
|
`lunora doctor` reports `paid-product-bound` (a metered binding is declared:
|
|
49
49
|
Workers AI, Browser, Images), `observability-full-sampling`,
|
|
50
|
+
`observability-query-unredacted` (request logs or traces keep the `?token=` in auth links),
|
|
50
51
|
`r2-lifecycle-unset`, `cpu-limit-missing` and `do-class-budget-*`.
|
|
51
52
|
`lunora advisor` lint ids are listed below.
|
|
52
53
|
|
|
@@ -185,10 +185,13 @@ See `lunora-migration-helper` for the full flow.
|
|
|
185
185
|
|
|
186
186
|
```bash
|
|
187
187
|
lunora cloudflare deployments list
|
|
188
|
+
lunora cloudflare deployments status # which versions carry traffic, and how much
|
|
188
189
|
lunora cloudflare deployments rollback --yes # previous version
|
|
189
190
|
lunora cloudflare deployments promote <version-id> --yes # send 100% of traffic to a version
|
|
190
191
|
```
|
|
191
192
|
|
|
193
|
+
A partial rollout (`--percent N --from <live-version-id> --confirm-shared-state`) runs two versions against one Durable Object database. Nothing in `promote` compares the two versions' schemas, so confirm them yourself before using it. Two facts bound the risk: `lunora deploy --preview` runs the schema-drift gate before it uploads, so a version uploaded that way passed it unless `--prebuilt` skipped the gate or `--allow-schema-drift` overrode it; and `wrangler versions upload` refuses a version with a pending Durable Object migration, so that migration reaches traffic only through `deploy`, at 100%. A split whose live version predates a migration the target relies on is not detected by either check.
|
|
194
|
+
|
|
192
195
|
Rolling back code does not roll back data migrations or D1 DDL.
|
|
193
196
|
|
|
194
197
|
## Common pitfalls
|
|
@@ -106,7 +106,9 @@ throws `` `queue` binding is required for mailer.queue() ``, except under
|
|
|
106
106
|
capture, where it sends straight to the capture inbox so dev flows keep working.
|
|
107
107
|
A consumer builds its mailer with `createMailerFromEnv` too, drains the queue
|
|
108
108
|
with `consumeQueuedSend(mailer, message.body)` and then calls `message.ack()`
|
|
109
|
-
for each message.
|
|
109
|
+
for each message. On a throw, branch on `isRetryableMailError(error)`: retry with
|
|
110
|
+
`message.retry()` when it is true, and ack and log when it is false, since a refused
|
|
111
|
+
address or malformed body never sends and a retry only dead-letters it. With a queue bound in dev, the message still goes through the
|
|
110
112
|
queue and the consumer's mailer captures it into the inbox. Queues deliver at least once: with
|
|
111
113
|
per-message acks a later throw retries only the unacknowledged messages, while
|
|
112
114
|
without them a single throw resends the whole batch. The full wiring is in the "Queueing"
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{r as A,c as g}from"./cloudflare-credentials-BVmZnDZk.mjs";import{spawnSync as x}from"node:child_process";import{readFileSync as p,writeFileSync as P,existsSync as w}from"node:fs";import{join as E}from"node:path";import{DEV_VARS_FILE as u,parseDevVariableEntries as k,upsertDevVariableLine as N,generateSecretValue as L,writeDevVariablesFileAtomically as F,isPlaceholderValue as y,isMintableSecretKey as K,mintableSecretKeysFor as U,secretSourcesFromBindings as M,inferLunoraBindings as W,resolveSchemaDirectory as V}from"@lunora/config";import{modify as B,applyEdits as Y,parse as z}from"jsonc-parser";import{r as H}from"./admin-token-VdUnvnKW.mjs";import{defaultSpawner as G}from"./createRecordingSpawner-01ZngIwK.mjs";import{toolchainExecArgs as J,detectPackageManager as X}from"@lunora/config/cli";const b=5e3,D=(e,t,s)=>`POST https://api.cloudflare.com/client/v4/accounts/${e}/artifacts/namespaces with body { "namespace": "${t}", "jurisdiction": "${s}" }`,q=e=>{let t="";try{const{errors:s}=JSON.parse(e.text);t=(s??[]).flatMap(n=>typeof n.message=="string"?[n.message]:[]).slice(0,2).join("; ")}catch{}return t===""?`HTTP ${String(e.status)}`:`HTTP ${String(e.status)}: ${t}`},Q=async(e,t,s)=>{const{jurisdiction:n,namespace:r}=e;try{const a=await g({accountId:t,apiToken:s,...e.fetch===void 0?{}:{fetch:e.fetch},init:{body:JSON.stringify({jurisdiction:n,namespace:r}),method:"POST",signal:AbortSignal.timeout(b)},path:"/artifacts/namespaces"});return a.ok?{}:{reason:q(a)}}catch(a){return{reason:a instanceof Error?a.message:String(a)}}},Z=async(e,t,s)=>{const n=await g({accountId:t,apiToken:s,...e.fetch===void 0?{}:{fetch:e.fetch},init:{signal:AbortSignal.timeout(b)},path:`/artifacts/namespaces/${encodeURIComponent(e.namespace)}`}).catch(()=>{}),r=n?.ok===!0?n.body.result:void 0;return typeof r=="object"&&r!==null&&"jurisdiction"in r?r.jurisdiction:void 0},j=(e,t,s,n)=>({fix:`A namespace's jurisdiction cannot change: create one with ${D(e,"<name>",s)}, point the binding's "namespace" at it, and move the repos over.`,message:`${t} is in the "${n}" jurisdiction, but the schema pins data to "${s}".`,verdict:"mismatch"}),ee=async(e,t,s,n)=>{const{jurisdiction:r,namespace:a}=e;let i;if(e.create===!0){if(i=(await Q(e,t,s)).reason,i===void 0)return{changesAccount:!0,message:`created ${n} in the "${r}" jurisdiction, as the schema pins.`,verdict:"ok"};const l=await Z(e,t,s);if(l===r)return{message:`${n} is in the "${r}" jurisdiction, as the schema pins.`,verdict:"ok"};if(typeof l=="string")return j(t,n,r,l)}if(e.missingOk===!0)return{changesAccount:!0,message:`${n} does not exist yet; it will be created in the "${r}" jurisdiction just before the deploy.`,verdict:"ok"};const c=i===void 0?"":` (${i})`,o=e.create===!0?` and could not be created${c}`:"";return{fix:`Create it before the first repo: ${D(t,a,r)}.`,message:`${n} does not exist${o}, and the first repo create() would make it unrestricted — the schema pins data to "${r}".`,verdict:"missing"}},De=async e=>{const{binding:t,jurisdiction:s,namespace:n}=e,r=`Artifacts namespace "${n}" (binding ${t})`,{accountId:a,token:i}=A(e.environment??process.env,e.accountId);if(i===void 0||a===void 0)return{fix:"Set CLOUDFLARE_API_TOKEN (with Artifacts edit access, which creating a missing namespace needs) and CLOUDFLARE_ACCOUNT_ID (or `account_id` in wrangler.jsonc) to check it.",message:`the jurisdiction of ${r} was not checked against the schema's "${s}".`,verdict:"unchecked"};let c;try{c=await g({accountId:a,apiToken:i,...e.fetch===void 0?{}:{fetch:e.fetch},init:{signal:AbortSignal.timeout(b)},path:`/artifacts/namespaces/${encodeURIComponent(n)}`})}catch(l){return{message:`the jurisdiction of ${r} could not be read (${l instanceof Error?l.message:String(l)}).`,verdict:"unchecked"}}if(!c.ok&&c.status===404)return ee(e,a,i,r);if(!c.ok)return{...c.status===401||c.status===403?{fix:`Give CLOUDFLARE_API_TOKEN Artifacts edit access on account ${a}.`}:{},message:`the jurisdiction of ${r} could not be read (HTTP ${String(c.status)}).`,verdict:"unchecked"};const{result:o}=c.body,d=typeof o=="object"&&o!==null&&"jurisdiction"in o?o.jurisdiction:void 0;return typeof d!="string"?{message:`the jurisdiction of ${r} could not be read (the response carried none).`,verdict:"unchecked"}:d===s?{message:`${r} is in the "${d}" jurisdiction, as the schema pins.`,verdict:"ok"}:j(a,r,s,d)},m=100,f=500,S=10,te=5e3,h="Delete the Workers you no longer need (old preview / staging deploys free their classes), or set `durableObjects: { merge: true }` in lunora.config before an app's first deploy so it spends one class instead of up to three.",ne=e=>e>=f?{fix:h,message:`the Cloudflare account has ${String(e)} Durable Object classes — the Workers Paid cap is ${String(f)}, so a deploy that adds one will fail.`,verdict:"full"}:e>=f-S?{fix:h,message:`the Cloudflare account has ${String(e)} of ${String(f)} Durable Object classes (Workers Paid cap).`,verdict:"near"}:e>=m-S&&e<=m?{fix:h,message:`the Cloudflare account has ${String(e)} Durable Object classes — on Workers Free the cap is ${String(m)} (Paid: ${String(f)}).`,verdict:"near"}:{message:`the Cloudflare account has ${String(e)} Durable Object classes (caps: ${String(m)} Free, ${String(f)} Paid).`,verdict:"ok"},je=async(e={})=>{const{accountId:t,token:s}=A(e.environment??process.env,e.accountId);if(s===void 0||t===void 0)return{fix:"Set CLOUDFLARE_API_TOKEN (Workers Scripts Read) and CLOUDFLARE_ACCOUNT_ID (or `account_id` in wrangler.jsonc) to check it.",message:"the account's Durable Object class count was not checked.",verdict:"unchecked"};try{const n=await g({accountId:t,apiToken:s,...e.fetch===void 0?{}:{fetch:e.fetch},init:{signal:AbortSignal.timeout(te)},path:"/workers/durable_objects/namespaces?per_page=1"}),r=n.ok?n.body.result_info:void 0,a=typeof r=="object"&&r!==null&&"total_count"in r?r.total_count:void 0;return typeof a!="number"?{message:`the account's Durable Object class count could not be read${n.ok?"":` (HTTP ${String(n.status)})`}.`,verdict:"unchecked"}:ne(a)}catch(n){return{message:`the account's Durable Object class count could not be read (${n instanceof Error?n.message:String(n)}).`,verdict:"unchecked"}}},$=e=>e.endsWith("S")?e.slice(0,-1):e,T=e=>e.replaceAll(/([a-z\d])([A-Z])/gu,"$1_$2").toUpperCase().split(/[_-]+/u).filter(t=>t.length>0),C=new Set(["CREDENTIAL","DSN","PASSPHRASE","PASSWD","PASSWORD","SECRET","TOKEN"]),O=["ACCESS_KEY","API_KEY","ENCRYPTION_KEY","PRIVATE_KEY","SIGNING_KEY"],re=new RegExp(`(?:${[...C,...O.map(e=>e.replaceAll("_",""))].join("|")})S?$`,"iu"),se=e=>T(e).some(t=>t==="PUBLIC"||t==="PUBLISHABLE"),ae=e=>{if(se(e))return!1;const t=T(e);if(t.some(n=>C.has($(n))))return!0;const s=$(t.join("_"));return O.some(n=>s===n||s.endsWith(`_${n}`))||re.test(e)},R=e=>{const t=e.trim();return t===""||t.startsWith("<")&&t.endsWith(">")},ie=/replace[-_\s]?(?:with|me)(?![a-z])/iu,Te=e=>R(e)||ie.test(e),ce=e=>R(e)||e.trim().toLowerCase().includes("replace"),I=/\r?\n/u,oe=async(e,t)=>{const{args:s,command:n}=J(X(t),{args:e,tool:"wrangler"});try{const r=await G({args:s,captureStderr:!0,captureStdoutSilently:!0,command:n,cwd:t});return{code:r.code,stderr:r.stderr??"",stdout:r.stdout??""}}catch(r){return{code:1,stderr:r instanceof Error?r.message:String(r),stdout:""}}},v=async(e,t)=>{const s=await e(["d1","list","--json"],t);if(s.code!==0)return;let n;try{n=JSON.parse(s.stdout)}catch{return}if(!Array.isArray(n))return;const r=new Map;for(const a of n)typeof a.name=="string"&&typeof a.uuid=="string"&&r.set(a.name,a.uuid);return r},de=e=>{const t=z(e);return(Array.isArray(t?.d1_databases)?t.d1_databases:[]).flatMap((n,r)=>{if(n===null||typeof n!="object")return[];const a=n,i=typeof a.database_id=="string"?a.database_id:"";return ce(i)?[{binding:typeof a.binding=="string"&&a.binding!==""?a.binding:"<unnamed>",index:r,name:typeof a.database_name=="string"&&a.database_name!==""?a.database_name:void 0}]:[]})},ue=async(e,t,s,n)=>{const r=await v(s,t.cwd);if(r===void 0){n.skipped.push(`D1 "${e.binding}": could not run \`wrangler d1 list --json\`. Check that wrangler is installed in this project and logged in (\`wrangler login\`), then retry.`);return}const a=r.get(e.name);if(a!==void 0)return a;if(t.dryRun){n.applied.push(`would run \`wrangler d1 create ${e.name}\` and write its id into ${e.binding}`);return}const i=await s(["d1","create",e.name],t.cwd);if(i.code!==0){const d=i.stderr.trim()||`exit ${String(i.code)}`;n.skipped.push(`D1 "${e.binding}": \`wrangler d1 create ${e.name}\` failed: ${d}`);return}const o=(await v(s,t.cwd))?.get(e.name);return o===void 0&&n.skipped.push(`D1 "${e.binding}": created ${e.name} but could not read back its id.`),o},le=async(e,t)=>{if(e.configPath===void 0)return;const s=e.runner??oe,n=p(e.configPath,"utf8"),r=de(n);let a=n;for(const i of r){if(i.name===void 0){t.skipped.push(`D1 "${i.binding}" has no database_name, so there is no database to create or reuse.`);continue}const c=await ue({...i,name:i.name},e,s,t);if(c!==void 0){const o=B(a,["d1_databases",i.index,"database_id"],c,{formattingOptions:{eol:`
|
|
2
|
-
`,insertSpaces:!0,tabSize:4}});a=Y(a,o),t.applied.push(`${i.binding}: database_id set to the id of D1 "${i.name}"`)}}!e.dryRun&&a!==n&&P(e.configPath,a)},fe=e=>{const t=x("git",["check-ignore","-q",u],{cwd:e,stdio:"ignore"});if(t.status===0)return!0;if(t.status===1)return!1;const s=E(e,".gitignore");return w(s)&&p(s,"utf8").split(I).some(n=>n.trim()===u)},me=async(e,t)=>{try{return U(M(await W({projectRoot:e,schemaDir:V(e)})))}catch{t.skipped.push(`${u}: could not read the project's bindings, so the relay key was not checked against its role.`);return}},ge=(e,t,s,n,r)=>{const a=[],i=new Set(t.split(I).flatMap(c=>{const[o]=k(c);return o!==void 0&&!y(o.value)?[o.key]:[]}));H({cwd:e}).source===void 0&&!i.has("LUNORA_ADMIN_TOKEN")&&a.push("LUNORA_ADMIN_TOKEN");for(const c of s)!ae(c.key)||!y(c.value)||i.has(c.key)||(K(c.key,c.value,r)?a.push(c.key):n.skipped.push(`${u}: ${c.key} is a provider key and cannot be minted here; fill it in by hand.`));return a},he=async(e,t)=>{const s=E(e.cwd,u),n=w(s)?p(s,"utf8"):"",r=ge(e.cwd,n,k(n),t,await me(e.cwd,t));if(r.length===0)return;if(!fe(e.cwd)){t.skipped.push(`${u} is not in .gitignore, so no secret is written there. Add \`${u}\` to .gitignore first.`);return}if(e.dryRun){t.applied.push(`would mint ${r.join(", ")} into ${u}`);return}let a=n;for(const i of r)a=N(a,i,L());F(s,a),t.applied.push(`minted ${r.join(", ")} into ${u}`)},Ce=async e=>{const t={applied:[],skipped:[]};return await le(e,t),await he(e,t),t},Oe=e=>{switch(e){case"boolean":return"boolean";case"date":case"number":case"timestamp":return"number";case"id":case"literal":case"string":return"string";default:return}},pe=/already (?:exists|indexed)|duplicate/iu,_=e=>["vectorize","create-metadata-index",e.index,`--property-name=${e.property}`,`--type=${e.type}`],be=async(e,t)=>{try{return await e(t)}catch(s){return{code:1,stderr:s instanceof Error?s.message:String(s)}}},Re=async e=>{const{cwd:t,entries:s,exec:n,logger:r,spawner:a}=e,i=[];for(const c of s){const o=await be(a,{args:[...n.args,..._(c)],captureStderr:!0,command:n.command,cwd:t,stdoutToStderr:!0});if(o.code===0){i.push({entry:c,status:"created"});continue}const d=`${o.stderr??""}${o.stdout??""}`;if(pe.test(d)){i.push({entry:c,status:"exists"});continue}i.push({entry:c,error:d.trim()||`exit code ${String(o.code)}`,status:"failed"}),r.warn(`could not create the Vectorize metadata index for "${c.property}" on "${c.index}" — filters on that property will match nothing until it exists. Run: wrangler ${_(c).join(" ")}`)}return i};export{Ce as a,ce as b,ae as c,_ as d,je as e,De as f,Re as g,Te as i,Oe as m};
|