@truefoundry/tfy-infra-cli 0.1.3 → 0.1.4-canary.595d398

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,13 +18,10 @@ npx @truefoundry/tfy-infra-cli tpl render -c config.yaml
18
18
 
19
19
  | Command | Description |
20
20
  |---------|-------------|
21
- | `tfy-init tpl render` | Render a template to HCL files |
22
- | `tfy-init tpl schema` | Fetch and display a template's JSON Schema |
21
+ | `tfy-init tpl render` | Render, upgrade, or re-generate template files (auto-detects mode) |
23
22
  | `tfy-init tpl validate` | Validate a local template (for authors) |
24
23
  | `tfy-init tpl test` | Run test fixtures against a template |
25
24
  | `tfy-init tpl verify` | Check integrity of files on disk against a manifest |
26
- | `tfy-init tpl upgrade` | Upgrade platform files to a new template version |
27
- | `tfy-init tpl hash` | Compute expected aggregate hash without generating files |
28
25
  | `tfy-init tpl dev` | Watch mode for rapid template iteration |
29
26
 
30
27
  For detailed usage, options, exit codes, and examples for each command, see the **[CLI Reference](../../docs/cli-reference.md)**.
@@ -32,13 +29,14 @@ For detailed usage, options, exit codes, and examples for each command, see the
32
29
  ## Supported Template Sources
33
30
 
34
31
  - **Local files**: `file:///absolute/path` or `file://./relative/path`
35
- - **GitHub**: `github://owner/repo/path/v1/1.0.0`
32
+ - **HTTPS bundles**: `https://registry.example.com/template/bundle.json`
36
33
 
37
34
  ## Global Options
38
35
 
39
36
  ```bash
40
37
  --verbose Enable verbose output
41
38
  -q, --quiet Suppress non-error output
39
+ --json Structured JSON to stdout (human logs to stderr)
42
40
  -v, --version Show version
43
41
  -h, --help Display help
44
42
  ```
@@ -73,8 +71,9 @@ All commands use a shared set of exit codes:
73
71
  | 2 | FETCH_ERROR | Template fetch failed (not found, network) |
74
72
  | 3 | RENDER_ERROR | Template rendering failed |
75
73
  | 4 | FORMAT_ERROR | `tofu fmt` failed |
74
+ | 5 | DRIFT_BLOCKED | Source mismatch (`--allow-source-change`) or content drift (`--force`) blocked upgrade |
76
75
 
77
- Some commands define additional codes for domain-specific outcomes (e.g., `DRIFT_DETECTED` for `verify`, `DRIFT_BLOCKED` for `upgrade`). See the [CLI Reference](../../docs/cli-reference.md) for details.
76
+ Some commands define additional codes for domain-specific outcomes (e.g., `DRIFT_DETECTED` for `verify`, `DRIFT_BLOCKED` for `render` in upgrade mode). Exit code 1 is also returned when overwrite confirmation is declined. See the [CLI Reference](../../docs/cli-reference.md) for details.
78
77
 
79
78
  ## License
80
79
 
package/dist/index.js CHANGED
@@ -1,16 +1,20 @@
1
1
  #!/usr/bin/env node
2
- "use strict";var Le=Object.create;var Ut=Object.defineProperty;var Je=Object.getOwnPropertyDescriptor;var ze=Object.getOwnPropertyNames;var We=Object.getPrototypeOf,Ge=Object.prototype.hasOwnProperty;var Be=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ze(t))!Ge.call(e,o)&&o!==r&&Ut(e,o,{get:()=>t[o],enumerable:!(n=Je(t,o))||n.enumerable});return e};var V=(e,t,r)=>(r=e!=null?Le(We(e)):{},Be(t||!e||!e.__esModule?Ut(r,"default",{value:e,enumerable:!0}):r,e));var Ve=require("commander");var Me=require("commander");var ee=require("commander"),wt=require("fs/promises"),Z=require("path"),re=V(require("js-yaml")),vt=require("@truefoundry/tfy-infra-engine");var qe={timeout:3e4,retries:3,noCache:!1};function ut(e){return{...qe,...e}}async function pt(e,t,r=1e3){let n;for(let o=0;o<=t;o++)try{return await e()}catch(i){if(n=i,o<t){let u=r*Math.pow(2,o)*(.5+Math.random()*.5);await new Promise(s=>setTimeout(s,u))}}throw n instanceof Error?n:new Error(String(n))}function rt(e,t,r){return Promise.race([e,new Promise((n,o)=>setTimeout(()=>o(new Error(r??`Operation timed out after ${t}ms`)),t))])}function $t(e){let t=/\/(\d+(?:\.\d+){0,2})$/,r=e.match(t);return r?.[1]?{semver:r[1]}:null}var lt=require("@truefoundry/tfy-infra-engine");var jt=require("fs/promises"),Ct=require("path"),R=require("@truefoundry/tfy-infra-engine");var mt=require("fs/promises"),bt=require("path");async function G(e){let t=new Map,r=new Map;return await Mt(e,"",t,r),{files:t,staticFiles:r}}async function Mt(e,t,r,n){let o=t?(0,bt.join)(e,t):e,i=await(0,mt.readdir)(o,{withFileTypes:!0});for(let u of i){if(u.name.startsWith("_"))continue;let s=t?`${t}/${u.name}`:u.name;if(u.isDirectory())await Mt(e,s,r,n);else{let c=await(0,mt.readFile)((0,bt.join)(o,u.name),"utf-8");if(u.name.endsWith(".hbs")){let f=s.replace(/\.hbs$/,"");r.set(f,c)}else n.set(s,c)}}}var ot=class{canResolve(t){return t.startsWith("file://")}async resolve(t,r){let n=this.uriToPath(t);try{let o=(0,Ct.join)(n,"template.json"),i=await this.readFileWithError(o,"template.json"),u;try{u=JSON.parse(i)}catch(y){throw new R.EngineError(`Failed to parse template.json: ${y.message}`,R.EngineErrorCode.TEMPLATE_JSON_INVALID,y,{path:o})}let{metadata:s,jsonSchema:c}=(0,R.validateTemplateJson)(u),f=(0,Ct.join)(n,"src"),m,p;try{({files:m,staticFiles:p}=await G(f))}catch(y){throw y.code==="ENOENT"?new R.EngineError(`Template src/ directory not found in ${n}`,R.EngineErrorCode.TEMPLATE_NOT_FOUND,y,{path:f}):y}if(m.size===0&&p.size===0)throw new R.EngineError(`No template files found in ${f}`,R.EngineErrorCode.TEMPLATE_NOT_FOUND,void 0,{path:f});let l={semver:s.version,apiVersion:this.extractApiVersionFromUri(t)};return{metadata:s,jsonSchema:c,files:m,staticFiles:p,source:t,version:l}}catch(o){if(o instanceof R.EngineError)throw o;let i=o;throw i.code==="ENOENT"?new R.EngineError(`Template not found: ${n}`,R.EngineErrorCode.FILE_NOT_FOUND,o,{path:n,originalCode:i.code}):i.code==="EACCES"?new R.EngineError(`Permission denied: ${n}`,R.EngineErrorCode.FILE_NOT_FOUND,o,{path:n,originalCode:i.code}):new R.EngineError(`Failed to load template from ${n}: ${o.message}`,R.EngineErrorCode.FILE_NOT_FOUND,o,{path:n})}}uriToPath(t){return t.startsWith("file:///")||t.startsWith("file://")?t.substring(7):t}async readFileWithError(t,r){try{return await(0,jt.readFile)(t,"utf-8")}catch(n){throw n.code==="ENOENT"?new R.EngineError(`Required file not found: ${r}`,R.EngineErrorCode.TEMPLATE_JSON_NOT_FOUND,n,{path:t,filename:r}):n}}extractApiVersionFromUri(t){let n=this.uriToPath(t).split("/").filter(Boolean),o=n[n.length-1]??"";return/^v\d+$/.test(o)?o:void 0}};var U=require("fs/promises"),H=require("path"),Vt=require("stream/promises"),Ht=require("zlib"),Lt=require("tar"),h=require("@truefoundry/tfy-infra-engine");function It(e,t){if(!e)return`v${t}`;let r=e.split("/"),o=(r.at(-1)?.match(/^v\d+$/)?r.slice(0,-1):r).join("-");return o?`${o}-v${t}`:`v${t}`}function Xe(e){let t=e.split("/").at(-1);return t?.match(/^v\d+$/)?t:void 0}function Ke(e){let t=e.split(".").length;return t===3?"exact":t===2?"minor":"major"}function Jt(e){if(!e.startsWith("github://"))throw new h.EngineError(`Invalid GitHub URI: ${e}`,h.EngineErrorCode.GITHUB_NOT_FOUND);let r=e.substring(9).split("/");if(r.length<4)throw new h.EngineError(`Invalid GitHub URI - expected format github://org/repo/path/semver: ${e}`,h.EngineErrorCode.GITHUB_NOT_FOUND);let n=r[0]??"",o=r[1]??"",u=r.slice(2).join("/").match(/^(.*?)\/(\d+(?:\.\d+){0,2})$/);if(!u)throw new h.EngineError(`Invalid GitHub URI - could not extract version: ${e}`,h.EngineErrorCode.GITHUB_NOT_FOUND);let s=u[1]??"",c=u[2]??"0",f=Ke(c),m=It(s,c);return{owner:n,repo:o,path:s,version:{semver:c,tag:m,pinType:f}}}function zt(e){return`https://github.com/${e.owner}/${e.repo}/archive/refs/tags/${e.version.tag}.tar.gz`}async function Wt(e,t){if(e.version.pinType==="exact")return e.version.tag;let r=It(e.path,e.version.semver),n=`https://api.github.com/repos/${e.owner}/${e.repo}/git/matching-refs/tags/${r}`,o={"User-Agent":"tfy-infra-engine",Accept:"application/vnd.github.v3+json"},i=process.env.GITHUB_TOKEN||process.env.GH_TOKEN;i&&(o.Authorization=`token ${i}`);let u=await fetch(n,{headers:o});if(!u.ok)throw u.status===403?new h.EngineError("GitHub rate limit exceeded during version resolution. Try again later or set GITHUB_TOKEN.",h.EngineErrorCode.GITHUB_RATE_LIMITED):new h.EngineError(`GitHub API error during version resolution: HTTP ${u.status}`,h.EngineErrorCode.NETWORK_ERROR,void 0,{apiUrl:n,status:u.status});let s=await u.json(),c=/v(\d+\.\d+\.\d+)$/,f=[];for(let m of s){let p=m.ref.replace("refs/tags/",""),l=p.match(c);if(l?.[1]){let y=l[1].split(".").map(Number);y.length===3&&f.push({tag:p,parts:y})}}if(f.length===0)throw new h.EngineError(`No tags matching ${r}* found in ${e.owner}/${e.repo}`,h.EngineErrorCode.GITHUB_NOT_FOUND,void 0,{tagPrefix:r,apiUrl:n});return f.sort((m,p)=>{for(let l=0;l<3;l++){let y=(m.parts[l]??0)-(p.parts[l]??0);if(y!==0)return y}return 0}),f.at(-1).tag}var nt=class{tempDir;constructor(t){let r=process.env.HOME||process.env.USERPROFILE||"/tmp";this.tempDir=t??(0,H.join)(r,".cache","tfy-infra","github-temp")}canResolve(t){return t.startsWith("github://")}async resolve(t,r){let n=ut(r),o=Jt(t),i=o.version.tag;o.version.pinType!=="exact"&&(i=await rt(Wt(o,r),n.timeout,`Version resolution timed out after ${n.timeout}ms`));let u={...o,version:{...o.version,tag:i}},s=zt(u),c=(0,H.join)(this.tempDir,`${o.owner}-${o.repo}-${Date.now()}`);try{await(0,U.mkdir)(c,{recursive:!0}),await rt(pt(()=>this.downloadAndExtract(s,c),n.retries),n.timeout,`GitHub download timed out after ${n.timeout}ms`);let f=await(0,U.readdir)(c);if(f.length===0)throw new h.EngineError(`Empty archive from GitHub: ${t}`,h.EngineErrorCode.GITHUB_NOT_FOUND,void 0,{archiveUrl:s});let m=(0,H.join)(c,f[0]??""),p=o.path?(0,H.join)(m,o.path):m,l=(0,H.join)(p,"template.json"),y;try{y=await(0,U.readFile)(l,"utf-8")}catch{throw new h.EngineError(`template.json not found in GitHub template: ${t}`,h.EngineErrorCode.TEMPLATE_JSON_NOT_FOUND,void 0,{templateDir:p})}let $;try{$=JSON.parse(y)}catch(k){throw new h.EngineError(`Failed to parse template.json: ${k.message}`,h.EngineErrorCode.TEMPLATE_JSON_INVALID,k,{templateDir:p})}let{metadata:C,jsonSchema:M}=(0,h.validateTemplateJson)($),P=(0,H.join)(p,"src"),z,j;try{({files:z,staticFiles:j}=await G(P))}catch{throw new h.EngineError(`Template src/ directory not found in GitHub archive: ${t}`,h.EngineErrorCode.TEMPLATE_NOT_FOUND,void 0,{templateDir:p,srcDir:P})}if(z.size===0&&j.size===0)throw new h.EngineError(`No template files found in src/ for GitHub: ${t}`,h.EngineErrorCode.TEMPLATE_NOT_FOUND,void 0,{srcDir:P});let W={semver:C.version,apiVersion:Xe(o.path)};return{metadata:C,jsonSchema:M,files:z,staticFiles:j,source:t,version:W}}catch(f){if(f instanceof h.EngineError)throw f;let m=f;throw m.message?.includes("404")||m.status===404?new h.EngineError(`GitHub template not found: ${t}`,h.EngineErrorCode.GITHUB_NOT_FOUND,f,{archiveUrl:s}):m.message?.includes("rate limit")||m.status===403?new h.EngineError("GitHub rate limit exceeded. Try again later or use authentication.",h.EngineErrorCode.GITHUB_RATE_LIMITED,f):new h.EngineError(`GitHub error: ${f.message}`,h.EngineErrorCode.NETWORK_ERROR,f,{uri:t,archiveUrl:s})}finally{try{await(0,U.rm)(c,{recursive:!0,force:!0})}catch{}}}async downloadAndExtract(t,r){let n=await fetch(t,{headers:{"User-Agent":"tfy-infra-engine"}});if(!n.ok)throw new Error(`HTTP ${n.status}: ${n.statusText}`);if(!n.body)throw new Error("Empty response body");let o=n.body;await(0,Vt.pipeline)(o,(0,Ht.createGunzip)(),(0,Lt.extract)({cwd:r}))}};var D=require("@truefoundry/tfy-infra-engine");var Gt="JFROG_ACCESS_TOKEN";function Bt(e){try{let{hostname:t}=new URL(e);return t.includes("jfrog")}catch{return!1}}function Ye(e){let t={"User-Agent":"tfy-infra-cli",Accept:"application/json"};if(Bt(e)){let r=process.env[Gt];r&&(t.Authorization=`Bearer ${r}`)}return t}async function Ze(e,t){let r=await fetch(e,{headers:t});if(r.status===401||r.status===403){let n=Bt(e)?` Set ${Gt} in your environment to authenticate.`:"";throw new D.EngineError(`Authentication failed (HTTP ${r.status}) for ${e}.${n}`,D.EngineErrorCode.HTTPS_AUTH_FAILED,void 0,{uri:e,status:r.status})}if(!r.ok)throw new D.EngineError(`Bundle download failed: HTTP ${r.status} for ${e}`,D.EngineErrorCode.NETWORK_ERROR,void 0,{uri:e,status:r.status});return r}async function Qe(e,t){let n=await e.json(),{metadata:o,jsonSchema:i}=(0,D.validateTemplateJson)({template:n.metadata,schema:n.jsonSchema}),u=new Map(Object.entries(n.files??{})),s=new Map(Object.entries(n.staticFiles??{}));if(u.size===0&&s.size===0)throw new D.EngineError(`Bundle contains no template files: ${t}`,D.EngineErrorCode.TEMPLATE_NOT_FOUND,void 0,{uri:t});let c={semver:o.version};return{metadata:o,jsonSchema:i,files:u,staticFiles:s,source:t,version:c}}var it=class{canResolve(t){return t.startsWith("https://")&&t.toLowerCase().endsWith(".json")}async resolve(t,r){let n=ut(r),o=Ye(t),i=await rt(pt(()=>Ze(t,o),n.retries),n.timeout,`HTTPS bundle download timed out after ${n.timeout}ms`);return Qe(i,t)}};var E=require("fs/promises"),T=require("path"),qt=require("crypto"),B=require("@truefoundry/tfy-infra-engine");function Xt(){let e=process.env.HOME||process.env.USERPROFILE||"~";return(0,T.join)(e,".cache","tfy-infra","templates")}var st=class{cacheDir;constructor(t){this.cacheDir=t??Xt()}getCacheKey(t){return(0,qt.createHash)("sha256").update(t).digest("hex").substring(0,16)}getCachePath(t,r){let n=this.getCacheKey(t),o=r.replace(/\//g,"-");return(0,T.join)(this.cacheDir,n,o)}async has(t,r){let n=this.getCachePath(t,r);try{let o=(0,T.join)(n,"template.json");return await(0,E.stat)(o),!0}catch{return!1}}async get(t,r){let n=this.getCachePath(t,r);try{let o=(0,T.join)(n,"template.json"),i=await(0,E.readFile)(o,"utf-8"),u=JSON.parse(i),{metadata:s,jsonSchema:c}=(0,B.validateTemplateJson)(u),f=new Map,m=new Map;await this.readCacheDir(n,"",f,m);let p={semver:s.version};return{metadata:s,jsonSchema:c,files:f,staticFiles:m,source:t,version:p}}catch{return null}}async set(t){let r=this.getCachePath(t.source,t.version.semver);try{await(0,E.mkdir)(r,{recursive:!0});let n={template:{name:t.metadata.name,version:t.metadata.version,...t.metadata.description?{description:t.metadata.description}:{}},schema:t.jsonSchema};await(0,E.writeFile)((0,T.join)(r,"template.json"),JSON.stringify(n,null,2),"utf-8");for(let[o,i]of t.files){let u=(0,T.join)(r,`${o}.hbs`);await(0,E.mkdir)((0,T.dirname)(u),{recursive:!0}),await(0,E.writeFile)(u,i,"utf-8")}for(let[o,i]of t.staticFiles){let u=(0,T.join)(r,o);await(0,E.mkdir)((0,T.dirname)(u),{recursive:!0}),await(0,E.writeFile)(u,i,"utf-8")}}catch(n){throw new B.EngineError(`Failed to cache template: ${n.message}`,B.EngineErrorCode.CACHE_ERROR,n,{source:t.source,version:t.version.semver})}}async delete(t,r){if(r){let n=this.getCachePath(t,r);try{await(0,E.rm)(n,{recursive:!0,force:!0})}catch{}}else{let n=this.getCacheKey(t),o=(0,T.join)(this.cacheDir,n);try{await(0,E.rm)(o,{recursive:!0,force:!0})}catch{}}}async clear(){try{await(0,E.rm)(this.cacheDir,{recursive:!0,force:!0})}catch{}}async stats(){let t=0,r=0,n=0;try{let o=await(0,E.readdir)(this.cacheDir);t=o.length;for(let i of o){let u=(0,T.join)(this.cacheDir,i),s=await(0,E.readdir)(u);r+=s.length;for(let c of s){let f=(0,T.join)(u,c);n+=await this.dirSize(f)}}}catch{}return{sources:t,templates:r,size:n}}async readCacheDir(t,r,n,o){let i=r?(0,T.join)(t,r):t,u=await(0,E.readdir)(i,{withFileTypes:!0});for(let s of u){if(s.name==="template.json"||s.name.startsWith("_"))continue;let c=r?`${r}/${s.name}`:s.name;if(s.isDirectory())await this.readCacheDir(t,c,n,o);else{let f=await(0,E.readFile)((0,T.join)(i,s.name),"utf-8");s.name.endsWith(".hbs")?n.set(c.replace(/\.hbs$/,""),f):o.set(c,f)}}}async dirSize(t){let r=0,n=await(0,E.readdir)(t,{withFileTypes:!0});for(let o of n){let i=(0,T.join)(t,o.name);if(o.isDirectory())r+=await this.dirSize(i);else{let u=await(0,E.stat)(i);r+=u.size}}return r}};var Ft=class{resolvers=[];cache;constructor(t,r){this.cache=new st(r),t?this.resolvers=t:(this.register(new ot),this.register(new nt),this.register(new it))}register(t){this.resolvers.push(t)}getResolver(t){return this.resolvers.find(r=>r.canResolve(t))}async resolve(t,r){let n=this.getResolver(t);if(!n){let i=t.split("://")[0]||"unknown";throw new lt.EngineError(`No resolver found for URI scheme: ${i}://`,lt.EngineErrorCode.TEMPLATE_NOT_FOUND,void 0,{uri:t,scheme:i,supportedSchemes:["file://","github://","https://*.json"]})}if(!r?.noCache&&!t.startsWith("file://")){let i=$t(t);if(i){let u=await this.cache.get(t,i.semver);if(u)return u}}let o=await n.resolve(t,r);return t.startsWith("file://")||await this.cache.set(o),o}canResolve(t){return this.resolvers.some(r=>r.canResolve(t))}getSupportedSchemes(){return["file://","github://","https://*.json"]}async clearCache(t){t?await this.cache.delete(t):await this.cache.clear()}async getCacheStats(){return this.cache.stats()}};function I(e){return new Ft(void 0,e)}var Yt=require("fs/promises"),Zt=V(require("js-yaml"));var St=require("fs/promises"),Kt=require("path");async function Pt(e){try{return await(0,St.access)(e),!0}catch{return!1}}async function q(e){let t=(0,Kt.join)(e,"tests","default","inputs.yaml");try{return await(0,St.access)(t),t}catch{return null}}async function X(e){let t=await(0,Yt.readFile)(e,"utf-8"),r=e.endsWith(".json"),n;try{r?n=JSON.parse(t):n=Zt.load(t)}catch(o){let i=r?"JSON":"YAML";throw new Error(`Failed to parse ${i} config file: ${o.message}`,{cause:o})}if(!n.intentId||typeof n.intentId!="string"||n.intentId.trim()==="")throw new Error(`Configuration file is missing required field 'intentId'. Add 'intentId: "<your-cluster-identity>"' to ${e}. The intentId uniquely identifies the cluster for integrity tracking.`);return{template:n.template,inputs:n.inputs??{},intentId:n.intentId,platformPrefix:n.platformPrefix,options:n.options}}var Qt=require("fs/promises");async function _(e){let t;try{t=await(0,Qt.readFile)(e,"utf-8")}catch(o){throw o.code==="ENOENT"?new Error(`Manifest not found: ${e}`,{cause:o}):new Error(`Failed to read manifest: ${o.message}`,{cause:o})}let r;try{r=JSON.parse(t)}catch{throw new Error(`Invalid JSON in manifest: ${e}`)}let n=["manifestVersion","files","aggregateHash","inputs","platformPrefix","intentId","templateSource","templateVersion"];for(let o of n)if(r[o]===void 0)throw new Error(`Manifest is missing required field: ${o}`);return r}function K(e){if(!e.inputs)throw new Error("Cannot reconstruct envelope: manifest is missing inputs field");if(!e.platformPrefix)throw new Error("Cannot reconstruct envelope: manifest is missing platformPrefix field");return{template:e.templateSource,inputs:e.inputs,intentId:e.intentId,platformPrefix:e.platformPrefix}}var at=require("fs/promises"),dt=require("path");async function Y(e,t,r){await(0,at.mkdir)(e,{recursive:!0});for(let[n,o]of t.files){let i=(0,dt.join)(e,n);r==="upgrade"&&o.zone==="user"&&await Pt(i)||(await(0,at.mkdir)((0,dt.dirname)(i),{recursive:!0}),await(0,at.writeFile)(i,o.content,"utf-8"))}}var F=require("@truefoundry/tfy-infra-engine");var L="normal";function Dt(e){L=e}function Nt(){return L}function v(e){L!=="quiet"&&console.log(`\u2713 ${e}`)}function w(e){console.error(`\u2717 ${e}`)}function a(e){L!=="quiet"&&console.log(e)}function g(e){L==="verbose"&&console.log(` ${e}`)}function te(e){L!=="quiet"&&console.warn(`\u26A0 ${e}`)}function tr(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function gt(e){return e.map(t=>` \u2022 ${t.path}: ${t.message}`).join(`
3
- `)}function ht(e){switch(e){case"content_drift":return"DRIFTED";case"missing_file":return"MISSING";case"unexpected_file":return"UNEXPECTED";case"metadata_inconsistency":return"INCONSISTENT";case"source_mismatch":return"SOURCE MISMATCH"}}function yt(e){if(L!=="quiet"){console.log("Files:");for(let t of e)console.log(` - ${t.path} (${tr(t.size)})`)}}var d={SUCCESS:0,VALIDATION_ERROR:1,FETCH_ERROR:2,RENDER_ERROR:3,FORMAT_ERROR:4};function b(e,t=d.RENDER_ERROR){if(e instanceof F.EngineError){if(w(e.message),Nt()==="verbose"&&e.details){a(""),a("Details:");for(let[r,n]of Object.entries(e.details))r!=="errors"&&a(` ${r}: ${JSON.stringify(n)}`)}switch(e.details?.errors&&(a(""),a("Errors:"),a(gt(e.details.errors))),e.code){case F.EngineErrorCode.INPUT_VALIDATION_FAILED:case F.EngineErrorCode.SCHEMA_INVALID:case F.EngineErrorCode.ENVELOPE_VALIDATION_FAILED:process.exit(d.VALIDATION_ERROR);break;case F.EngineErrorCode.TEMPLATE_NOT_FOUND:case F.EngineErrorCode.FILE_NOT_FOUND:case F.EngineErrorCode.NETWORK_ERROR:case F.EngineErrorCode.GITHUB_NOT_FOUND:process.exit(d.FETCH_ERROR);break;case F.EngineErrorCode.TOFU_NOT_FOUND:case F.EngineErrorCode.TOFU_FMT_FAILED:a(""),a("Run with --skip-format to skip formatting."),process.exit(d.FORMAT_ERROR);break;case F.EngineErrorCode.MANIFEST_PARSE_ERROR:process.exit(d.VALIDATION_ERROR);break;default:process.exit(t)}}w(e.message),Nt()==="verbose"&&console.error(e.stack),process.exit(t)}function er(e){if([e.config,e.fromManifest].filter(Boolean).length>1)throw new Error("--config and --from-manifest are mutually exclusive. Use one or the other.");if(e.config&&e.directory!==".")throw new Error("--config and --directory are mutually exclusive. Use one or the other.")}function oe(){return new ee.Command("render").description("Render a template to HCL files").option("-c, --config <path>","Path to envelope config file (YAML or JSON)").option("-d, --directory <dir>","Path to template version directory",".").option("-f, --fixture <path>","Input file to use (when using --directory)").option("-i, --input <key=value>","Inline input override (can be repeated)",rr,[]).requiredOption("-o, --output <dir>","Output directory").option("--from-manifest <path>","Path to manifest.json to use as input source").option("--skip-format","Skip tofu fmt formatting").option("--no-cache","Force re-fetch template").option("--timeout <ms>","Network timeout in milliseconds","30000").option("--intent-id <id>","Cluster identity for integrity tracking").action(async t=>{try{t.config&&(t.config=(0,Z.resolve)(t.config)),t.fromManifest&&(t.fromManifest=(0,Z.resolve)(t.fromManifest)),er({config:t.config,directory:t.directory,fromManifest:t.fromManifest});let r=await or(t);process.exit(r)}catch(r){b(r)}})}function rr(e,t){return[...t,e]}function ne(e){let t={};if(!e)return t;for(let r of e){let n=r.indexOf("=");if(n>0){let o=r.substring(0,n),i=r.substring(n+1);t[o]=i}}return t}async function or(e){let t=(0,Z.resolve)(e.output),r=parseInt(e.timeout,10);if(e.fromManifest)return nr(e.fromManifest,e,t,r);if(e.config)return sr(e.config,e,t,r);let n=(0,Z.resolve)(e.directory);return ir(n,e,t,r)}async function nr(e,t,r,n){g(`Loading manifest from ${e}`);let o=await _(e),i=K(o);t.intentId&&(i.intentId=t.intentId),v(`Template: ${i.template}`),g(`Intent ID: ${i.intentId}`),g("Resolving template...");let c={template:await I().resolve(i.template,{noCache:t.cache===!1,timeout:n}),inputs:i.inputs,intentId:i.intentId,platformPrefix:i.platformPrefix,options:{skipFormat:t.skipFormat}};g("Re-rendering from manifest...");let m=await(0,vt.createEngine)().install(c);v("Inputs validated"),v(`Generated ${m.files.size-1} files + manifest.json`),t.skipFormat||v("Formatted with tofu fmt"),await Y(r,m,"install");let p=[];for(let[l,y]of m.files)p.push({path:l,size:Buffer.byteLength(y.content,"utf-8")});return a(""),a(`Output written to: ${r}`),a(`Aggregate Hash: ${m.manifest.aggregateHash}`),yt(p),d.SUCCESS}async function ir(e,t,r,n){let o=`file://${e}`,i=t.fixture?(0,Z.resolve)(e,t.fixture):null;if(i)try{await(0,wt.access)(i)}catch{return w(`Fixture not found: ${t.fixture}`),d.VALIDATION_ERROR}else if(i=await q(e),!i)return w("No default fixture found at tests/default/inputs.yaml"),a("Either create a default fixture or use --fixture to specify one."),d.VALIDATION_ERROR;let u=await(0,wt.readFile)(i,"utf-8"),s=re.load(u),c=ne(t.input);s={...s,...c};let f=t.intentId??s.intentId??`local-${Date.now()}`;v(`Template: ${o}`),g(`Fixture: ${i}`),g(`Intent ID: ${f}`),g("Resolving template...");let l={template:await I().resolve(o,{noCache:t.cache===!1,timeout:n}),inputs:s,intentId:f,options:{skipFormat:t.skipFormat}},y=(0,vt.createEngine)();g("Validating inputs...");let $=await y.install(l);v("Inputs validated"),v(`Generated ${$.files.size-1} files + manifest.json`),t.skipFormat||v("Formatted with tofu fmt"),await Y(r,$,"install");let C=[];for(let[M,P]of $.files)C.push({path:M,size:Buffer.byteLength(P.content,"utf-8")});return a(""),a(`Output written to: ${r}`),a(`Aggregate Hash: ${$.manifest.aggregateHash}`),yt(C),d.SUCCESS}async function sr(e,t,r,n){g(`Loading config from ${e}`);let o=await X(e);if(t.intentId&&(o.intentId=t.intentId),t.input&&t.input.length>0){let p=ne(t.input);o.inputs={...o.inputs||{},...p}}v(`Template: ${o.template}`),g(`Intent ID: ${o.intentId}`),g("Resolving template...");let s={template:await I().resolve(o.template,{noCache:t.cache===!1,timeout:n}),inputs:o.inputs,intentId:o.intentId,platformPrefix:o.platformPrefix,options:{skipFormat:t.skipFormat??o.options?.skipFormat}},c=(0,vt.createEngine)();g("Validating inputs...");let f=await c.install(s);v("Inputs validated"),v(`Generated ${f.files.size-1} files + manifest.json`),s.options?.skipFormat||v("Formatted with tofu fmt"),await Y(r,f,"install");let m=[];for(let[p,l]of f.files)m.push({path:p,size:Buffer.byteLength(l.content,"utf-8")});return a(""),a(`Output written to: ${r}`),a(`Aggregate Hash: ${f.manifest.aggregateHash}`),yt(m),d.SUCCESS}var ie=require("commander"),Et=require("fs/promises"),ct=require("path"),se=V(require("js-yaml"));function ae(){return new ie.Command("schema").description("Fetch and display the schema for a template").option("--template <uri>","Template source URI (github://, file://)").option("--format <type>","Output format: json, yaml","json").option("-o, --output <file>","Output file path (default: inputs.schema.json in template dir)").option("--full","Include full template.json (metadata + schema)").option("--no-cache","Force re-fetch template").option("--timeout <ms>","Network timeout in milliseconds","30000").action(async t=>{try{if(!t.template)throw new Error("--template <uri> is required.");t.output&&(t.output=(0,ct.resolve)(t.output));let r=await cr(t);process.exit(r)}catch(r){b(r)}})}async function ar(e,t){await(0,Et.mkdir)((0,ct.dirname)(t),{recursive:!0}),await(0,Et.writeFile)(t,JSON.stringify(e,null,2),"utf-8")}async function cr(e){let t=e.template,r=parseInt(e.timeout,10);g(`Fetching schema from ${t}`);let o=await I().resolve(t,{noCache:e.cache===!1,timeout:r}),i;return e.full?i={template:{name:o.metadata.name,version:o.metadata.version,...o.metadata.description?{description:o.metadata.description}:{}},schema:o.jsonSchema}:i=o.jsonSchema,e.output?(await ar(i,e.output),v(`JSON Schema written to: ${e.output}`),d.SUCCESS):(e.format==="yaml"?console.log(se.dump(i,{indent:2,lineWidth:100})):console.log(JSON.stringify(i,null,2)),d.SUCCESS)}var ce=require("commander"),fe=require("fs/promises"),ft=require("path"),ue=V(require("handlebars")),Q=require("@truefoundry/tfy-infra-engine");function pe(){return new ce.Command("validate").description("Validate a template (for template authors)").option("-d, --directory <dir>","Path to local template directory",".").option("--strict","Fail on warnings").option("--skip-format","Skip tofu fmt validation").action(async t=>{try{let r=await fr(t);process.exit(r)}catch(r){b(r)}})}async function fr(e){let t=(0,ft.resolve)(e.directory),r=[],n=!1;a(`Validating template at: ${t}`),a(""),g("Checking template.json...");let o=(0,ft.join)(t,"template.json");try{let c=await(0,fe.readFile)(o,"utf-8"),f;try{f=JSON.parse(c)}catch(p){throw new Error(`Invalid JSON: ${p.message}`,{cause:p})}let{jsonSchema:m}=(0,Q.validateTemplateJson)(f);(0,Q.validateJsonSchemaStructure)(m),v("template.json is valid")}catch(c){r.push({type:"error",message:c.message,file:"template.json"}),n=!0}g("Checking template files in src/...");let i=(0,ft.join)(t,"src");try{let{files:c,staticFiles:f}=await G(i);if(c.size===0&&f.size===0)r.push({type:"error",message:"No template files found in src/"}),n=!0;else{for(let[m,p]of c)try{ue.default.precompile(p),g(` \u2713 ${m}.hbs`)}catch(l){r.push({type:"error",message:`Invalid Handlebars syntax: ${l.message}`,file:`src/${m}.hbs`}),n=!0}v(`Found ${c.size} HBS template(s)`+(f.size>0?` and ${f.size} static file(s)`:"")+" with valid syntax")}}catch(c){r.push({type:"error",message:`Cannot read src/ directory: ${c.message}`}),n=!0}e.skipFormat||(g("Checking tofu fmt availability..."),await(0,Q.isTofuAvailable)()?v("tofu fmt available"):r.push({type:"warning",message:"tofu not found - format validation skipped"})),a("");let u=r.filter(c=>c.type==="warning"),s=r.filter(c=>c.type==="error");if(u.length>0){te(`${u.length} warning(s):`);for(let c of u){let f=c.file?`${c.file}: `:"";a(` \u2022 ${f}${c.message}`)}a("")}if(s.length>0){w(`${s.length} error(s):`);for(let c of s){let f=c.file?`${c.file}: `:"";a(` \u2022 ${f}${c.message}`)}a("")}return n?(w("Template validation failed"),d.VALIDATION_ERROR):e.strict&&u.length>0?(w("Template validation failed (strict mode)"),d.VALIDATION_ERROR):(v("Template is valid"),d.SUCCESS)}var le=require("commander"),S=require("fs/promises"),N=require("path"),de=V(require("js-yaml")),ge=require("@truefoundry/tfy-infra-engine");var me=require("diff");function Rt(e,t,r){let n=e.replace(/\r\n/g,`
2
+ "use strict";var kt=Object.create;var Ae=Object.defineProperty;var _t=Object.getOwnPropertyDescriptor;var At=Object.getOwnPropertyNames;var Vt=Object.getPrototypeOf,Mt=Object.prototype.hasOwnProperty;var Ut=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of At(t))!Mt.call(e,o)&&o!==r&&Ae(e,o,{get:()=>t[o],enumerable:!(n=_t(t,o))||n.enumerable});return e};var B=(e,t,r)=>(r=e!=null?kt(Vt(e)):{},Ut(t||!e||!e.__esModule?Ae(r,"default",{value:e,enumerable:!0}):r,e));var Dt=require("commander");var Pt=require("commander");var Le=require("commander"),z=require("fs/promises"),J=require("path");var ce=require("fs/promises"),Ee=require("path");async function ue(e){let t=new Map,r=new Map;return await Ve(e,"",t,r),{files:t,staticFiles:r}}async function Ve(e,t,r,n){let o=t?(0,Ee.join)(e,t):e,i=await(0,ce.readdir)(o,{withFileTypes:!0});for(let a of i){if(a.name.startsWith("_"))continue;let c=t?`${t}/${a.name}`:a.name;if(a.isDirectory())await Ve(e,c,r,n);else{let f=await(0,ce.readFile)((0,Ee.join)(o,a.name),"utf-8");if(a.name.endsWith(".hbs")){let u=c.replace(/\.hbs$/,"");r.set(u,f)}else n.set(c,f)}}}var T=require("@truefoundry/tfy-infra-engine");var A="normal",X=!1;function be(e){A=e}function xe(){return A}function Me(e){X=e}function D(e){process.stdout.write(e+`
3
+ `)}function R(e){A!=="quiet"&&(X?console.error:console.log)(`\u2713 ${e}`)}function h(e){console.error(`\u2717 ${e}`)}function s(e){A!=="quiet"&&(X?console.error:console.log)(e)}function w(e){A==="verbose"&&(X?console.error:console.log)(` ${e}`)}function k(e){A!=="quiet"&&console.warn(`\u26A0 ${e}`)}function Lt(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function fe(e){return e.map(t=>` \u2022 ${t.path}: ${t.message}`).join(`
4
+ `)}function le(e){switch(e){case"content_drift":return"DRIFTED";case"missing_file":return"MISSING";case"unexpected_file":return"UNEXPECTED";case"metadata_inconsistency":return"INCONSISTENT";case"source_mismatch":return"SOURCE MISMATCH"}}function Ue(e){if(A==="quiet")return;let t=X?console.error:console.log;t("Files:");for(let r of e)t(` - ${r.path} (${Lt(r.size)})`)}var p={SUCCESS:0,VALIDATION_ERROR:1,FETCH_ERROR:2,RENDER_ERROR:3,FORMAT_ERROR:4,DRIFT_BLOCKED:5};function S(e,t=p.RENDER_ERROR){if(e instanceof T.EngineError){if(h(e.message),xe()==="verbose"&&e.details){s(""),s("Details:");for(let[r,n]of Object.entries(e.details))r!=="errors"&&s(` ${r}: ${JSON.stringify(n)}`)}switch(e.details?.errors&&(s(""),s("Errors:"),s(fe(e.details.errors))),e.code){case T.EngineErrorCode.INPUT_VALIDATION_FAILED:case T.EngineErrorCode.SCHEMA_INVALID:case T.EngineErrorCode.ENVELOPE_VALIDATION_FAILED:process.exit(p.VALIDATION_ERROR);break;case T.EngineErrorCode.TEMPLATE_NOT_FOUND:case T.EngineErrorCode.FILE_NOT_FOUND:case T.EngineErrorCode.NETWORK_ERROR:process.exit(p.FETCH_ERROR);break;case T.EngineErrorCode.TOFU_NOT_FOUND:case T.EngineErrorCode.TOFU_FMT_FAILED:s(""),s("Run with --skip-format to skip formatting."),process.exit(p.FORMAT_ERROR);break;case T.EngineErrorCode.MANIFEST_PARSE_ERROR:process.exit(p.VALIDATION_ERROR);break;default:process.exit(t)}}h(e.message),xe()==="verbose"&&console.error(e.stack),process.exit(t)}function Be(){return new Le.Command("bundle").description("Build a bundle.json from a schema package and src/ directory").requiredOption("--schema <path-or-dash>",'Schema package JSON file, or "-" for stdin').requiredOption("--src <dir>","Template source directory containing .hbs and static files").requiredOption("--name <string>","Template name").option("--description <string>","Template description").option("-o, --output <path>","Output file path (default: stdout)").action(async t=>{try{await Wt(t)}catch(r){S(r)}})}async function Bt(e){let t;e==="-"?t=await Jt():t=await(0,z.readFile)((0,J.resolve)(e),"utf-8");let r;try{r=JSON.parse(t)}catch(o){throw new Error(`Failed to parse schema package JSON: ${o.message}`,{cause:o})}let n=r;if(!n.jsonSchema||typeof n.jsonSchema!="object")throw new Error('Schema package must contain a "jsonSchema" object');if(!Array.isArray(n.uiSchema))throw new Error('Schema package must contain a "uiSchema" array');return{jsonSchema:n.jsonSchema,uiSchema:n.uiSchema}}function Jt(){return new Promise((e,t)=>{let r=[];process.stdin.on("data",n=>r.push(n)),process.stdin.on("end",()=>e(Buffer.concat(r).toString("utf-8"))),process.stdin.on("error",t)})}function zt(e){let t=e.properties;if(!t)return;let r=t.version;if(!r)return;let n=r.default;return typeof n=="string"?n:void 0}async function Wt(e){let t=await Bt(e.schema),r=e.version??zt(t.jsonSchema);if(!r)throw new Error('Could not determine template version. Provide --version or ensure the schema has a "version" property with a default value.');let n=(0,J.resolve)(e.src),{files:o,staticFiles:i}=await ue(n);if(o.size===0&&i.size===0)throw new Error(`No template files found in ${n}`);let a={name:e.name,version:r};e.description&&(a.description=e.description);let c={metadata:a,jsonSchema:t.jsonSchema,uiSchema:t.uiSchema,files:Object.fromEntries(o),staticFiles:Object.fromEntries(i),version:{semver:r}},f=JSON.stringify(c,null,2)+`
5
+ `;if(e.output){let u=(0,J.resolve)(e.output);await(0,z.mkdir)((0,J.dirname)(u),{recursive:!0}),await(0,z.writeFile)(u,f,"utf-8"),R(`Bundle written to ${u}`)}else process.stdout.write(f)}var st=require("commander"),at=require("readline"),U=require("fs/promises"),N=require("path"),ct=B(require("js-yaml")),oe=require("@truefoundry/tfy-infra-engine");var me=require("@truefoundry/tfy-infra-engine");var Y=require("fs/promises"),Oe=require("path"),E=require("@truefoundry/tfy-infra-engine"),Z=class{canResolve(t){return t.startsWith("file://")}async resolve(t,r){let n=this.uriToPath(t);try{let o=await(0,Y.stat)(n);if(o.isFile()&&n.endsWith(".json"))return this.resolveBundle(n,t);if(o.isDirectory()){let i=(0,Oe.join)(n,"build","bundle.json");if(await this.fileExists(i))return this.resolveBundle(i,t);let a=(0,Oe.join)(n,"bundle.json");if(await this.fileExists(a))return this.resolveBundle(a,t);throw new E.EngineError(`bundle.json not found in ${n} or ${n}/build`,E.EngineErrorCode.BUNDLE_NOT_FOUND,void 0,{path:n})}throw new E.EngineError(`Not a file or directory: ${n}`,E.EngineErrorCode.TEMPLATE_NOT_FOUND,void 0,{path:n})}catch(o){if(o instanceof E.EngineError)throw o;let i=o;throw i.code==="ENOENT"?new E.EngineError(`Template not found: ${n}`,E.EngineErrorCode.FILE_NOT_FOUND,o,{path:n,originalCode:i.code}):i.code==="EACCES"?new E.EngineError(`Permission denied: ${n}`,E.EngineErrorCode.FILE_NOT_FOUND,o,{path:n,originalCode:i.code}):new E.EngineError(`Failed to load template from ${n}: ${o.message}`,E.EngineErrorCode.FILE_NOT_FOUND,o,{path:n})}}async resolveBundle(t,r){let n=await(0,Y.readFile)(t,"utf-8"),o;try{o=JSON.parse(n)}catch(l){throw new E.EngineError(`Failed to parse bundle.json: ${l.message}`,E.EngineErrorCode.BUNDLE_INVALID,l,{path:t})}let i=o.metadata;if(!i||typeof i!="object")throw new E.EngineError('bundle.json must contain a "metadata" object',E.EngineErrorCode.BUNDLE_INVALID,void 0,{path:t});if(!i.name||!i.version)throw new E.EngineError('bundle.json metadata must contain "name" and "version"',E.EngineErrorCode.BUNDLE_INVALID,void 0,{path:t});let a={name:i.name,version:i.version,...i.description?{description:i.description}:{}},c=o.jsonSchema;if(!c||typeof c!="object")throw new E.EngineError('bundle.json must contain a "jsonSchema" object',E.EngineErrorCode.BUNDLE_INVALID,void 0,{path:t});let f=new Map(Object.entries(o.files??{})),u=new Map(Object.entries(o.staticFiles??{}));if(f.size===0&&u.size===0)throw new E.EngineError(`Bundle contains no template files: ${t}`,E.EngineErrorCode.TEMPLATE_NOT_FOUND,void 0,{path:t});let m={semver:a.version};return{metadata:a,jsonSchema:c,files:f,staticFiles:u,source:r,version:m}}uriToPath(t){return t.startsWith("file:///")||t.startsWith("file://")?t.substring(7):t}async fileExists(t){try{return await(0,Y.stat)(t),!0}catch{return!1}}};var C=require("@truefoundry/tfy-infra-engine");var Ht={timeout:3e4,retries:3,noCache:!1};function Je(e){return{...Ht,...e}}async function ze(e,t,r=1e3){let n;for(let o=0;o<=t;o++)try{return await e()}catch(i){if(n=i,o<t){let a=r*Math.pow(2,o)*(.5+Math.random()*.5);await new Promise(c=>setTimeout(c,a))}}throw n instanceof Error?n:new Error(String(n))}function We(e,t,r){let n,o=new Promise((i,a)=>{n=setTimeout(()=>a(new Error(r??`Operation timed out after ${t}ms`)),t)});return Promise.race([e,o]).finally(()=>clearTimeout(n))}var He="JFROG_ACCESS_TOKEN";function qe(e){try{let{hostname:t}=new URL(e);return t.includes("jfrog")}catch{return!1}}function qt(e){let t={"User-Agent":"tfy-infra-cli",Accept:"application/json"};if(qe(e)){let r=process.env[He];r&&(t.Authorization=`Bearer ${r}`)}return t}async function Gt(e,t){let r=await fetch(e,{headers:t});if(r.status===401||r.status===403){let n=qe(e)?` Set ${He} in your environment to authenticate.`:"";throw new C.EngineError(`Authentication failed (HTTP ${r.status}) for ${e}.${n}`,C.EngineErrorCode.HTTPS_AUTH_FAILED,void 0,{uri:e,status:r.status})}if(!r.ok)throw new C.EngineError(`Bundle download failed: HTTP ${r.status} for ${e}`,C.EngineErrorCode.NETWORK_ERROR,void 0,{uri:e,status:r.status});return r}async function Kt(e,t){let n=await e.json(),{metadata:o,jsonSchema:i}=(0,C.validateBundle)(n),a=new Map(Object.entries(n.files??{})),c=new Map(Object.entries(n.staticFiles??{}));if(a.size===0&&c.size===0)throw new C.EngineError(`Bundle contains no template files: ${t}`,C.EngineErrorCode.TEMPLATE_NOT_FOUND,void 0,{uri:t});let f={semver:o.version};return{metadata:o,jsonSchema:i,files:a,staticFiles:c,source:t,version:f}}var Q=class{canResolve(t){return t.startsWith("https://")&&t.toLowerCase().endsWith(".json")}async resolve(t,r){let n=Je(r),o=qt(t),i=await We(ze(()=>Gt(t,o),n.retries),n.timeout,`HTTPS bundle download timed out after ${n.timeout}ms`);return Kt(i,t)}};var b=require("fs/promises"),$=require("path"),Ge=require("crypto"),W=require("@truefoundry/tfy-infra-engine");function Ke(){let e=process.env.HOME||process.env.USERPROFILE||"~";return(0,$.join)(e,".cache","tfy-infra","templates")}var ee=class{cacheDir;constructor(t){this.cacheDir=t??Ke()}getCacheKey(t){return(0,Ge.createHash)("sha256").update(t).digest("hex").substring(0,16)}getCachePath(t,r){let n=this.getCacheKey(t),o=r.replace(/\//g,"-");return(0,$.join)(this.cacheDir,n,o)}async has(t,r){let n=this.getCachePath(t,r);try{let o=(0,$.join)(n,"bundle.json");return await(0,b.stat)(o),!0}catch{return!1}}async get(t,r){let n=this.getCachePath(t,r);try{let o=(0,$.join)(n,"bundle.json"),i=await(0,b.readFile)(o,"utf-8"),a=JSON.parse(i),{metadata:c,jsonSchema:f}=(0,W.validateBundle)(a),u=a,m=new Map(Object.entries(u.files??{})),l=new Map(Object.entries(u.staticFiles??{})),d={semver:c.version};return{metadata:c,jsonSchema:f,files:m,staticFiles:l,source:t,version:d}}catch{return null}}async set(t){let r=this.getCachePath(t.source,t.version.semver);try{await(0,b.mkdir)(r,{recursive:!0});let n={metadata:{name:t.metadata.name,version:t.metadata.version,...t.metadata.description?{description:t.metadata.description}:{}},jsonSchema:t.jsonSchema,files:Object.fromEntries(t.files),staticFiles:Object.fromEntries(t.staticFiles)};await(0,b.writeFile)((0,$.join)(r,"bundle.json"),JSON.stringify(n,null,2),"utf-8")}catch(n){throw new W.EngineError(`Failed to cache template: ${n.message}`,W.EngineErrorCode.CACHE_ERROR,n,{source:t.source,version:t.version.semver})}}async delete(t,r){if(r){let n=this.getCachePath(t,r);try{await(0,b.rm)(n,{recursive:!0,force:!0})}catch{}}else{let n=this.getCacheKey(t),o=(0,$.join)(this.cacheDir,n);try{await(0,b.rm)(o,{recursive:!0,force:!0})}catch{}}}async clear(){try{await(0,b.rm)(this.cacheDir,{recursive:!0,force:!0})}catch{}}async stats(){let t=0,r=0,n=0;try{let o=await(0,b.readdir)(this.cacheDir);t=o.length;for(let i of o){let a=(0,$.join)(this.cacheDir,i),c=await(0,b.readdir)(a);r+=c.length;for(let f of c){let u=(0,$.join)(a,f);n+=await this.dirSize(u)}}}catch{}return{sources:t,templates:r,size:n}}async dirSize(t){let r=0,n=await(0,b.readdir)(t,{withFileTypes:!0});for(let o of n){let i=(0,$.join)(t,o.name);if(o.isDirectory())r+=await this.dirSize(i);else{let a=await(0,b.stat)(i);r+=a.size}}return r}};var Te=class{resolvers=[];cache;constructor(t,r){this.cache=new ee(r),t?this.resolvers=t:(this.register(new Z),this.register(new Q))}register(t){this.resolvers.push(t)}getResolver(t){return this.resolvers.find(r=>r.canResolve(t))}async resolve(t,r){let n=this.getResolver(t);if(!n){let i=t.split("://")[0]||"unknown";throw new me.EngineError(`No resolver found for URI scheme: ${i}://`,me.EngineErrorCode.TEMPLATE_NOT_FOUND,void 0,{uri:t,scheme:i,supportedSchemes:["file://","https://*.json"]})}let o=await n.resolve(t,r);return t.startsWith("file://")||await this.cache.set(o),o}canResolve(t){return this.resolvers.some(r=>r.canResolve(t))}getSupportedSchemes(){return["file://","https://*.json"]}async clearCache(t){t?await this.cache.delete(t):await this.cache.clear()}async getCacheStats(){return this.cache.stats()}};function P(e){return new Te(void 0,e)}var Xt="https://tfy.jfrog.io/artifactory/tfy-template-stable",pe="latest",Xe=/^[a-zA-Z0-9_-]+$/;function Yt(e){let t=e.trim(),r=t.split("/");if(r.length!==2||!r[0]||!r[1]||!Xe.test(r[0])||!Xe.test(r[1]))throw new Error(`Invalid template name: "${e}". Expected format: "provider/template" (e.g. "aws/platform", "aws-core/template-name").`);return t}function Zt(e){if(e==null)return pe;let t=typeof e=="string"?e.trim():"";return t===""||t.toLowerCase()===pe?pe:t}function Qt(e,t){let r=(process.env.TEMPLATE_REGISTRY_URL??Xt).replace(/\/$/,"").trim(),n=Yt(e),o=Zt(t),i=o===pe?"[RELEASE]":encodeURIComponent(o);return`${r}/${n}/${i}/bundle.json`}function Ye(e){if(e.templateName&&e.templateVersion)return Qt(e.templateName,e.templateVersion);if(e.template)return e.template;throw new Error("Cannot resolve template: provide either templateName + templateVersion or template (URI).")}var Qe=require("fs/promises"),et=B(require("js-yaml"));var Ze=require("fs/promises"),Se=require("path");async function de(e){try{return await(0,Ze.access)(e),!0}catch{return!1}}async function H(e){let t=[(0,Se.join)(e,"tests","default","inputs.yaml"),(0,Se.join)(e,"tests","minimal","inputs.yaml")];for(let r of t)if(await de(r))return r;return null}async function Ie(e){let t=await(0,Qe.readFile)(e,"utf-8"),r=e.endsWith(".json"),n;try{r?n=JSON.parse(t):n=et.load(t)}catch(u){let m=r?"JSON":"YAML";throw new Error(`Failed to parse ${m} config file: ${u.message}`,{cause:u})}if(!n.intentId||typeof n.intentId!="string"||n.intentId.trim()==="")throw new Error(`Configuration file is missing required field 'intentId'. Add 'intentId: "<your-cluster-identity>"' to ${e}. The intentId uniquely identifies the cluster for integrity tracking.`);let o=n.templateName,i=n.templateVersion,a=n.template;if(!!!(o&&i)&&!!!a)throw o&&!i?new Error(`'templateName' requires 'templateVersion'. Add 'templateVersion: "<semver>"' to ${e}.`):i&&!o?new Error(`'templateVersion' requires 'templateName'. Add 'templateName: "<provider/template>"' to ${e}.`):new Error(`Configuration file must specify a template. Provide either 'templateName' + 'templateVersion' (registry mode) or 'template' (URI mode) in ${e}.`);return{template:a,templateName:o,templateVersion:i,inputs:n.inputs??{},intentId:n.intentId,platformPrefix:n.platformPrefix,options:n.options}}var tt=require("fs/promises");async function te(e){let t;try{t=await(0,tt.readFile)(e,"utf-8")}catch(o){throw o.code==="ENOENT"?new Error(`Manifest not found: ${e}`,{cause:o}):new Error(`Failed to read manifest: ${o.message}`,{cause:o})}let r;try{r=JSON.parse(t)}catch{throw new Error(`Invalid JSON in manifest: ${e}`)}let n=["manifestVersion","files","aggregateHash","inputs","platformPrefix","intentId","templateSource","templateVersion"];for(let o of n)if(r[o]===void 0)throw new Error(`Manifest is missing required field: ${o}`);return r}function rt(e){if(!e.inputs)throw new Error("Cannot reconstruct envelope: manifest is missing inputs field");if(!e.platformPrefix)throw new Error("Cannot reconstruct envelope: manifest is missing platformPrefix field");return{...!!(e.templateName&&e.templateVersion)?{templateName:e.templateName,templateVersion:e.templateVersion}:{template:e.templateSource},inputs:e.inputs,intentId:e.intentId,platformPrefix:e.platformPrefix}}var ge=require("fs/promises"),nt=require("path"),q=require("@truefoundry/tfy-infra-engine");async function he(e,t=q.DEFAULT_PREFIX){let r=new Map,n;try{n=(await(0,ge.readdir)(e,{withFileTypes:!0})).filter(i=>i.isFile()).map(i=>i.name)}catch(o){throw o.code==="ENOENT"?new Error(`Directory not found: ${e}`,{cause:o}):new Error(`Failed to read directory: ${o.message}`,{cause:o})}for(let o of n){if(o==="manifest.json")continue;let i=await(0,ge.readFile)((0,nt.join)(e,o),"utf-8"),a=(0,q.classifyFile)(o,t),c={content:i,zone:a};if(a==="platform"){let f=(0,q.parseHeader)(i);f&&(c.header=f)}r.set(o,c)}return r}var re=require("fs/promises"),ye=require("path");async function ne(e,t,r){await(0,re.mkdir)(e,{recursive:!0});for(let[n,o]of t.files){let i=(0,ye.join)(e,n);r==="upgrade"&&o.zone==="user"&&await de(i)||(await(0,re.mkdir)((0,ye.dirname)(i),{recursive:!0}),await(0,re.writeFile)(i,o.content,"utf-8"))}}var ot=require("diff");function M(e,t,r){let n=e.replace(/\r\n/g,`
4
6
  `),o=t.replace(/\r\n/g,`
5
- `);return(0,me.createTwoFilesPatch)(`expected/${r}`,`actual/${r}`,n,o,"","",{context:3})}function he(){return new le.Command("test").description("Run test fixtures against a template").option("-d, --directory <dir>","Path to local template directory",".").option("--fixture <name>","Run specific fixture only").option("--update","Update expected outputs with actual results").option("--diff","Show unified diff on failures").option("--skip-format","Skip tofu fmt in tests").action(async t=>{try{let r=await ur(t);process.exit(r)}catch(r){b(r)}})}async function ur(e){let t=(0,N.resolve)(e.directory),r=(0,N.join)(t,"tests"),n=`file://${t}`;a(`Testing template at: ${t}`),a("");let o;try{let p=await(0,S.readdir)(r);o=[];for(let l of p)try{let y=(0,N.join)(r,l,"inputs.yaml");await(0,S.readFile)(y),o.push(l)}catch{}}catch{return w(`No tests directory found at ${r}`),d.VALIDATION_ERROR}if(o.length===0)return w("No test fixtures found"),a(""),a("Expected structure:"),a(" tests/"),a(" <fixture-name>/"),a(" inputs.yaml"),a(" expected/"),d.VALIDATION_ERROR;if(e.fixture){if(!o.includes(e.fixture))return w(`Fixture '${e.fixture}' not found`),a(`Available fixtures: ${o.join(", ")}`),d.VALIDATION_ERROR;o=[e.fixture]}a(`Running ${o.length} fixture(s)...`),a("");let u=await I().resolve(n,{noCache:!0}),s=(0,ge.createEngine)(),c=[];for(let p of o){let l=(0,N.join)(r,p),y=(0,N.join)(l,"inputs.yaml"),$=(0,N.join)(l,"expected");g(`Running fixture: ${p}`);try{let C=await(0,S.readFile)(y,"utf-8"),M=de.load(C),P={template:u,inputs:M,intentId:`test-${p}`,options:{skipFormat:e.skipFormat}},z=await s.install(P);if(e.update)await pr($,z),v(`${p}: updated`),c.push({name:p,passed:!0});else{let{errors:j,diffs:W}=await mr($,z,e.diff);if(j.length===0)v(`${p}: passed`),c.push({name:p,passed:!0});else{w(`${p}: failed`);for(let k of j)a(` ${k}`);if(e.diff&&W&&W.length>0)for(let k of W){a(` --- expected/${k.filename}`),a(` +++ actual/${k.filename}`);let He=k.diff.split(`
6
- `).slice(4);for(let kt of He)kt&&a(` ${kt}`)}c.push({name:p,passed:!1,errors:j,diffs:W})}}}catch(C){w(`${p}: error`),a(` ${C.message}`),c.push({name:p,passed:!1,errors:[C.message]})}}a("");let f=c.filter(p=>p.passed).length,m=c.filter(p=>!p.passed).length;return m===0?(v(`All ${f} fixture(s) passed`),d.SUCCESS):(w(`${m} fixture(s) failed, ${f} passed`),d.VALIDATION_ERROR)}function _t(e){return e.replace(/("source"\s*:\s*)"file:\/\/[^"]*"/g,'$1"<local>"')}async function pr(e,t){await(0,S.mkdir)(e,{recursive:!0});for(let[r,n]of t.files){if(r==="manifest.json")continue;let o=(0,N.join)(e,r);await(0,S.writeFile)(o,_t(n.content),"utf-8")}}async function mr(e,t,r){let n=[],o=[];for(let[i,u]of t.files){if(i==="manifest.json")continue;let s=u.content,c=(0,N.join)(e,i);try{let f=await(0,S.readFile)(c,"utf-8"),m=_t(s.trim().replace(/\r\n/g,`
7
- `)),p=_t(f.trim().replace(/\r\n/g,`
8
- `));if(m!==p&&(n.push(`${i}: content mismatch`),r)){let l=Rt(p,m,i);o.push({filename:i,diff:l})}}catch{n.push(`${i}: expected file not found`)}}try{let i=await(0,S.readdir)(e);for(let u of i)!t.files.has(u)&&u!=="manifest.json"&&n.push(`${u}: unexpected file in expected/`)}catch{}return{errors:n,diffs:r?o:void 0}}var Re=require("commander"),O=require("fs/promises"),x=require("path"),Te=V(require("js-yaml")),tt=require("@truefoundry/tfy-infra-engine");var ye=V(require("chokidar")),we=require("path");function lr(e){let t=e.replace(/\\/g,"/"),r=(0,we.basename)(t);return r==="template.json"?"schema":/\/src\//.test(t)&&!r.startsWith("_")?"template":r==="inputs.yaml"?"inputs":"other"}function ve(e,t={}){let{onChange:r,onReady:n,onError:o,ignored:i=/(^|[/\\])\..|(^|[/\\])node_modules($|[/\\])|(^|[/\\])dev($|[/\\])/,debounceMs:u=100}=t,s=ye.default.watch(e,{ignored:i,persistent:!0,ignoreInitial:!0,awaitWriteFinish:{stabilityThreshold:u,pollInterval:50}}),c=f=>m=>{let p=lr(m);r&&r({type:f,path:m,fileType:p})};return s.on("change",c("change")),s.on("add",c("add")),s.on("unlink",c("unlink")),n&&s.on("ready",n),o&&s.on("error",f=>o(f instanceof Error?f:new Error(String(f)))),s}function xe(){return new Re.Command("dev").description("Watch mode for rapid template iteration").option("-d, --directory <dir>","Path to template version directory",".").option("-f, --fixture <path>","Input file or fixture to use for testing").requiredOption("-o, --output <dir>","Output directory for rendered files").option("--no-test","Only validate, do not run tests").action(async t=>{try{let r=await hr(t);process.exit(r)}catch(r){b(r)}})}async function dr(e){try{await(0,O.access)(e)}catch{return{valid:!1,error:`Directory not found: ${e}`}}let t=(0,x.join)(e,"template.json");try{await(0,O.access)(t)}catch{return{valid:!1,error:`Missing template.json in ${e}`}}return{valid:!0}}async function gr(e,t){let r=(0,x.resolve)(t);try{await(0,O.rm)(r,{recursive:!0,force:!0})}catch{}return await(0,O.mkdir)(r,{recursive:!0}),r}async function hr(e){let t=(0,x.resolve)(e.directory),r=await dr(t);if(!r.valid)return w(r.error),d.VALIDATION_ERROR;let n;if(e.fixture){n=(0,x.resolve)(t,e.fixture);try{await(0,O.access)(n)}catch{return w(`Fixture not found: ${e.fixture}`),d.VALIDATION_ERROR}}else{let f=await q(t);if(!f)return w("No default fixture found at tests/default/inputs.yaml"),a("Either create a default fixture or use --fixture to specify one."),d.VALIDATION_ERROR;n=f}let o=await gr(t,e.output),i=(0,x.relative)(process.cwd(),o);a(`[watching] ${(0,x.relative)(process.cwd(),t)}`),a(`[output] ${i}/`),a(`[fixture] ${(0,x.relative)(process.cwd(),n)}`),a("");let u=(0,tt.createEngine)(),s=`file://${t}`;await Ee(u,s,t,n,o,e.test);let c=ve(t,{onChange:f=>{(async()=>{let m=(0,x.relative)(t,f.path);a(`[changed] ${m}`),f.fileType==="schema"?await Oe(s):(f.fileType==="template"||f.fileType==="inputs")&&await Ee(u,s,t,n,o,e.test)})()},onReady:()=>{a("[ready] Watching for changes... (Ctrl+C to exit)"),a("")},onError:f=>{w(`Watch error: ${f.message}`)}});return new Promise(f=>{let m=()=>{a(""),a("[stopped] Watch mode ended"),c.close(),f(d.SUCCESS)};process.on("SIGINT",m),process.on("SIGTERM",m)})}async function Oe(e){try{return(await I().resolve(e,{noCache:!0})).jsonSchema?(a("[validate] \u2713 template.json valid"),!0):(w("[validate] \u2717 template.json invalid"),!1)}catch(t){return w("[validate] \u2717 template.json invalid"),a(` ${t.message}`),!1}}async function Ee(e,t,r,n,o,i){if(await Oe(t))try{let s=await(0,O.readFile)(n,"utf-8"),c=Te.load(s),p={template:await I().resolve(t,{noCache:!0}),inputs:c,intentId:`dev-${Date.now()}`,options:{skipFormat:!1}},l=await e.install(p),y=Array.from(l.files.keys()).filter(C=>C!=="manifest.json").length;a(`[validate] \u2713 ${y} template files valid`),await(0,O.rm)(o,{recursive:!0,force:!0}),await(0,O.mkdir)(o,{recursive:!0});let $=0;for(let[C,M]of l.files){if(C==="manifest.json")continue;let P=(0,x.join)(o,C);await(0,O.mkdir)((0,x.dirname)(P),{recursive:!0}),await(0,O.writeFile)(P,M.content,"utf-8"),$++}a(`[render] \u2713 ${$} files written to ${(0,x.relative)(process.cwd(),o)}/`),i!==!1&&await yr(r,l,o)}catch(s){w("[render] \u2717 Render failed"),a(` ${s.message}`),s instanceof tt.EngineError&&s.code===tt.EngineErrorCode.INPUT_VALIDATION_FAILED&&s.details?.errors&&a(gt(s.details.errors))}}async function yr(e,t,r){let n=(0,x.join)(e,"tests","default","expected"),o=Date.now();try{await(0,O.access)(n)}catch{a("[test] \u26A0 No expected/ directory, skipping comparison");return}let i=[];for(let[s,c]of t.files){if(s==="manifest.json")continue;let f=c.content,m=(0,x.join)(n,s);try{let p=await(0,O.readFile)(m,"utf-8"),l=f.trim().replace(/\r\n/g,`
9
- `),y=p.trim().replace(/\r\n/g,`
10
- `);if(l!==y){let $=Rt(y,l,s);i.push({file:s,diff:$})}}catch{i.push({file:s})}}let u=Date.now()-o;if(i.length===0)a(`[test] \u2713 default: passed (${u}ms)`);else{w("[test] \u2717 default: failed");for(let s of i)if(a(` ${s.file}: content mismatch`),s.diff){let c=s.diff.split(`
11
- `).slice(4,14);for(let f of c)f&&a(` ${f}`);s.diff.split(`
12
- `).length>14&&a(" ... (diff truncated)")}}}var be=require("commander"),Ot=require("path"),Ce=require("@truefoundry/tfy-infra-engine");var Tt=require("fs/promises"),$e=require("path"),et=require("@truefoundry/tfy-infra-engine");async function xt(e,t=et.DEFAULT_PREFIX){let r=new Map,n;try{n=(await(0,Tt.readdir)(e,{withFileTypes:!0})).filter(i=>i.isFile()).map(i=>i.name)}catch(o){throw o.code==="ENOENT"?new Error(`Directory not found: ${e}`,{cause:o}):new Error(`Failed to read directory: ${o.message}`,{cause:o})}for(let o of n){if(o==="manifest.json")continue;let i=await(0,Tt.readFile)((0,$e.join)(e,o),"utf-8"),u=(0,et.classifyFile)(o,t),s={content:i,zone:u};if(u==="platform"){let c=(0,et.parseHeader)(i);c&&(s.header=c)}r.set(o,s)}return r}var wr=1;function Ie(){return new be.Command("verify").description("Check integrity of files on disk against a manifest").option("-d, --directory <dir>","Path to cluster directory containing manifest.json",".").option("--json","Output drift report as structured JSON").action(async t=>{try{let r=await vr(t);process.exit(r)}catch(r){b(r)}})}async function vr(e){let t=(0,Ot.resolve)(e.directory),r=(0,Ot.join)(t,"manifest.json");g(`Loading manifest from ${r}`);let n=await _(r);g(`Reading files from ${t}`);let o=await xt(t,n.platformPrefix),u=await(0,Ce.createEngine)().verify(o,n);return e.json?a(Rr(u.driftReport)):a(Er(u.driftReport)),u.driftReport.valid?d.SUCCESS:wr}function Er(e){let t=[],r=e.summary.driftedFiles+e.summary.missingFiles+e.summary.unexpectedFiles;if(e.valid)t.push(`\u2713 Verification passed: ${e.summary.totalFiles} files checked, ${r} issues`),t.push(""),t.push(`Aggregate Hash: ${e.aggregateHash.expected}`);else{t.push(`\u2717 Drift detected: ${r} issues found`),t.push("");for(let n of e.entries){let o=ht(n.type);t.push(` ${o} ${n.path}`),t.push(` ${n.details}`),t.push("")}t.push(`Summary: ${e.summary.totalFiles} files checked, ${e.summary.driftedFiles} drifted, ${e.summary.missingFiles} missing`+(e.summary.unexpectedFiles>0?`, ${e.summary.unexpectedFiles} unexpected`:""))}return t.join(`
13
- `)}function Rr(e){return JSON.stringify(e,null,2)}var Fe=require("commander"),J=require("path"),Se=require("@truefoundry/tfy-infra-engine");var Pe=3;function Tr(e){if(e.config&&e.fromManifest)throw new Error("--config and --from-manifest are mutually exclusive. Use one or the other.");if(!e.config&&!e.fromManifest)throw new Error("Either --config <path> or --from-manifest <manifest-path> is required.");if(e.fromManifest&&!e.template)throw new Error("--from-manifest requires --template <new-uri> to specify the new template version.")}function De(){return new Fe.Command("upgrade").description("Upgrade platform files to a new template version").option("-c, --config <path>","Path to envelope config file (YAML or JSON)").option("-d, --directory <dir>","Directory containing current cluster files",".").option("-o, --output <dir>","Output directory for upgraded files (defaults to --directory)").option("--force","Proceed with upgrade even if drift is detected").option("--dry-run","Show what would change without writing files").option("--json","Output results as structured JSON").option("--from-manifest <path>","Path to manifest.json as input source").option("--template <uri>","New template URI (required with --from-manifest)").option("--skip-format","Skip tofu fmt formatting").option("--no-cache","Force re-fetch template").option("--timeout <ms>","Network timeout in milliseconds","30000").action(async t=>{try{t.config&&(t.config=(0,J.resolve)(t.config)),t.fromManifest&&(t.fromManifest=(0,J.resolve)(t.fromManifest)),Tr({config:t.config,fromManifest:t.fromManifest,template:t.template});let r=await xr(t);process.exit(r)}catch(r){b(r)}})}async function xr(e){let t=(0,J.resolve)(e.directory),r=e.output?(0,J.resolve)(e.output):t,n=parseInt(e.timeout,10),o=(0,J.join)(t,"manifest.json");g(`Loading manifest from ${o}`);let i=await _(o);g(`Reading files from ${t}`);let u=await xt(t,i.platformPrefix),s;if(e.fromManifest){let y=await _(e.fromManifest);s=K(y),s.template=e.template}else s=await X(e.config);v(`Template: ${s.template}`),g(`Intent ID: ${s.intentId}`),g("Resolving template...");let m={template:await I().resolve(s.template,{noCache:e.cache===!1,timeout:n}),inputs:s.inputs,intentId:s.intentId,platformPrefix:s.platformPrefix,options:{skipFormat:e.skipFormat??s.options?.skipFormat}},l=await(0,Se.createEngine)().upgrade(m,u,i);return l.sourceBlocked?(w("Upgrade blocked: template source mismatch"),a("The current files were generated from a different template source."),Pe):!l.driftReport.valid&&!e.force?Or(l,e.json):e.dryRun?$r(l,e.json):(await Y(r,l,"upgrade"),e.json?a(_e(l)):a(Ne(l)),d.SUCCESS)}function Or(e,t){let r=e.driftReport.summary.driftedFiles+e.driftReport.summary.missingFiles+e.driftReport.summary.unexpectedFiles;if(t)a(JSON.stringify({blocked:!0,reason:"drift_detected",driftReport:e.driftReport},null,2));else{w(`Upgrade blocked: drift detected in ${r} files`),a("");for(let n of e.driftReport.entries){let o=ht(n.type);a(` ${o} ${n.path}`),a(` ${n.details}`)}a(""),a("Use --force to override and proceed with upgrade.")}return Pe}function $r(e,t){return t?a(_e(e)):(a("Dry run \u2014 no files written."),a(""),a(Ne(e))),d.SUCCESS}function Ne(e){let t=[];t.push(`\u2713 Upgrade complete: ${e.manifest.templateSource} v${e.manifest.templateVersion}`),t.push("");let r=0,n=0;for(let[,o]of e.files)o.zone==="platform"?r++:n++;return t.push(` Updated: ${r} files (Platform Zone)`),t.push(` Unchanged: ${n} files (User Zone)`),t.push(""),t.push(`Aggregate Hash: ${e.manifest.aggregateHash}`),t.join(`
14
- `)}function _e(e){return JSON.stringify({success:!0,templateSource:e.manifest.templateSource,templateVersion:e.manifest.templateVersion,aggregateHash:e.manifest.aggregateHash,driftReport:e.driftReport,files:Array.from(e.files.entries()).map(([t,r])=>({path:t,zone:r.zone}))},null,2)}var Ae=require("commander"),At=require("path"),ke=require("@truefoundry/tfy-infra-engine");function Ue(){return new Ae.Command("hash").description("Compute the expected aggregate hash without generating files").option("-c, --config <path>","Path to envelope config file (YAML or JSON)").option("--from-manifest <path>","Path to manifest.json to use as input source").option("--json","Output result as structured JSON").option("--skip-format","Skip tofu fmt formatting").option("--no-cache","Force re-fetch template").option("--timeout <ms>","Network timeout in milliseconds","30000").action(async t=>{try{if(t.config&&(t.config=(0,At.resolve)(t.config)),t.fromManifest&&(t.fromManifest=(0,At.resolve)(t.fromManifest)),t.config&&t.fromManifest)throw new Error("--config and --from-manifest are mutually exclusive. Use one or the other.");if(!t.config&&!t.fromManifest)throw new Error("Either --config <path> or --from-manifest <manifest-path> is required.");let r=await br(t);process.exit(r)}catch(r){b(r)}})}async function br(e){let t=parseInt(e.timeout,10),r;if(e.fromManifest){g(`Loading manifest from ${e.fromManifest}`);let c=await _(e.fromManifest);r=K(c)}else g(`Loading config from ${e.config}`),r=await X(e.config);g(`Template: ${r.template}`),g("Resolving template...");let i={template:await I().resolve(r.template,{noCache:e.cache===!1,timeout:t}),inputs:r.inputs,intentId:r.intentId,platformPrefix:r.platformPrefix,options:{skipFormat:e.skipFormat??r.options?.skipFormat}},s=await(0,ke.createEngine)().hashOnly(i);return e.json?a(Ir(s)):a(Cr(s)),d.SUCCESS}function Cr(e){let t=[];return t.push(`Aggregate Hash: ${e.aggregateHash}`),t.push(`Template: ${e.templateSource}`),t.push(`Version: ${e.templateVersion}`),t.push(`Files: ${e.fileCount}`),t.join(`
15
- `)}function Ir(e){return JSON.stringify(e,null,2)}function je(){let e=new Me.Command("tpl").description("Template operations");return e.addCommand(xe()),e.addCommand(oe()),e.addCommand(ae()),e.addCommand(pe()),e.addCommand(he()),e.addCommand(Ie()),e.addCommand(De()),e.addCommand(Ue()),e}process.env.INIT_CWD&&process.chdir(process.env.INIT_CWD);var Fr="0.1.3";async function Sr(){let e=new Ve.Command;e.name("tfy-init").description("TrueFoundry infrastructure templating CLI").version(Fr,"-v, --version","Show version").option("--verbose","Enable verbose output").option("-q, --quiet","Suppress non-error output").hook("preAction",t=>{let r=t.opts();r.quiet?Dt("quiet"):r.verbose&&Dt("verbose")}),e.addCommand(je()),await e.parseAsync(process.argv)}Sr().catch(e=>{console.error("Fatal error:",e),process.exit(1)});
7
+ `);return(0,ot.createTwoFilesPatch)(`expected/${r}`,`actual/${r}`,n,o,"","",{context:3})}function Fe(e){let t=Ye(e);return R(e.templateName?`Template: ${e.templateName}@${e.templateVersion}`:`Template: ${t}`),t}function er(e){if([e.config,e.fromManifest].filter(Boolean).length>1)throw new Error("--config and --from-manifest are mutually exclusive. Use one or the other.");if(e.config&&e.directory!==".")throw new Error("--config and --directory are mutually exclusive. Use one or the other.")}function ut(){return new st.Command("render").description("Render a template to HCL files").option("-c, --config <path>","Path to envelope config file (YAML or JSON)").option("-d, --directory <dir>","Path to template version directory",".").option("-f, --fixture <path>","Input file to use (when using --directory)").option("-i, --input <key=value>","Inline input override (can be repeated)",tr,[]).requiredOption("-o, --output <dir>","Output directory").option("--from-manifest <path>","Path to manifest.json to use as input source").option("--force","Proceed even if managed files have drifted (upgrade mode)").option("--allow-source-change","Allow upgrade when template source has changed").option("--dry-run","Show what would change without writing files").option("--json","Output result as structured JSON").option("--skip-format","Skip tofu fmt formatting").option("--no-cache","Force re-fetch template").option("--timeout <ms>","Network timeout in milliseconds","30000").option("--intent-id <id>","Cluster identity for integrity tracking").action(async t=>{try{t.config&&(t.config=(0,N.resolve)(t.config)),t.fromManifest&&(t.fromManifest=(0,N.resolve)(t.fromManifest)),er({config:t.config,directory:t.directory,fromManifest:t.fromManifest});let r=await rr(t);process.exit(r)}catch(r){S(r)}})}function tr(e,t){return[...t,e]}function Ce(e){let t={};if(!e)return t;for(let r of e){let n=r.indexOf("=");if(n>0){let o=r.substring(0,n),i=r.substring(n+1);t[o]=i}}return t}async function rr(e){e.json&&Me(!0);let t=(0,N.resolve)(e.output),r=parseInt(e.timeout,10);if(e.fromManifest)return ir(e.fromManifest,e,t,r);if(e.config){let o=(0,N.join)(t,"manifest.json");return await ft(o)?cr(e.config,e,t,r,o):ar(e.config,e,t,r)}let n=(0,N.resolve)(e.directory);return sr(n,e,t,r)}async function ft(e){try{return await(0,U.access)(e),!0}catch{return!1}}async function nr(e){if(!process.stdin.isTTY)return!1;let t=(0,at.createInterface)({input:process.stdin,output:process.stderr});return new Promise(r=>{t.question(`${e} [y/N] `,n=>{t.close(),r(n.trim().toLowerCase()==="y")})})}async function or(e){try{return(await(0,U.readdir)(e,{withFileTypes:!0})).filter(r=>r.isFile()&&r.name!=="config.yaml"&&r.name!=="config.json").length}catch{return 0}}async function $e(e,t){if(t.force)return null;let r=await or(e);return r===0||(k(`Output directory contains ${r} existing files that will be overwritten.`),await nr("Proceed?"))?null:(t.json?D(JSON.stringify({aborted:!0,reason:"overwrite_confirmation",existingFiles:r},null,2)):s("Aborted. Use --force to skip confirmation."),p.VALIDATION_ERROR)}async function ir(e,t,r,n){w(`Loading manifest from ${e}`);let o=await te(e),i=rt(o);t.intentId&&(i.intentId=t.intentId);let a=Fe(i);w(`Intent ID: ${i.intentId}`),w("Resolving template...");let u={template:await P().resolve(a,{noCache:t.cache===!1,timeout:n}),inputs:i.inputs,intentId:i.intentId,platformPrefix:i.platformPrefix,options:{skipFormat:t.skipFormat}};w("Re-rendering from manifest...");let l=await(0,oe.createEngine)().install(u);if(t.dryRun)return await Pe(l,r,t.json);let d=await $e(r,t);return d!==null?d:(R("Inputs validated"),R(`Generated ${l.files.size-1} files + manifest.json`),t.skipFormat||R("Formatted with tofu fmt"),await ne(r,l,"install"),je(l,r,t.json),p.SUCCESS)}async function sr(e,t,r,n){let o=`file://${e}`,i=t.fixture?(0,N.resolve)(e,t.fixture):null;if(i){if(!await ft(i))return h(`Fixture not found: ${t.fixture}`),p.VALIDATION_ERROR}else if(i=await H(e),!i)return h("No default fixture found at tests/default/inputs.yaml"),s("Either create a default fixture or use --fixture to specify one."),p.VALIDATION_ERROR;let a=await(0,U.readFile)(i,"utf-8"),c=ct.load(a),f=Ce(t.input);c={...c,...f};let u=t.intentId??c.intentId??`local-${Date.now()}`;R(`Template: ${o}`),w(`Fixture: ${i}`),w(`Intent ID: ${u}`),w("Resolving template...");let d={template:await P().resolve(o,{noCache:t.cache===!1,timeout:n}),inputs:c,intentId:u,options:{skipFormat:t.skipFormat}},g=(0,oe.createEngine)();w("Validating inputs...");let O=await g.install(d);if(t.dryRun)return await Pe(O,r,t.json);let x=await $e(r,t);return x!==null?x:(R("Inputs validated"),R(`Generated ${O.files.size-1} files + manifest.json`),t.skipFormat||R("Formatted with tofu fmt"),await ne(r,O,"install"),je(O,r,t.json),p.SUCCESS)}async function ar(e,t,r,n){w(`Loading config from ${e}`);let o=await Ie(e);if(t.intentId&&(o.intentId=t.intentId),t.input&&t.input.length>0){let d=Ce(t.input);o.inputs={...o.inputs||{},...d}}let i=Fe(o);w(`Intent ID: ${o.intentId}`),w("Resolving template...");let f={template:await P().resolve(i,{noCache:t.cache===!1,timeout:n}),inputs:o.inputs,intentId:o.intentId,platformPrefix:o.platformPrefix,options:{skipFormat:t.skipFormat??o.options?.skipFormat}},u=(0,oe.createEngine)();w("Validating inputs...");let m=await u.install(f);if(t.dryRun)return await Pe(m,r,t.json);let l=await $e(r,t);return l!==null?l:(R("Inputs validated"),R(`Generated ${m.files.size-1} files + manifest.json`),f.options?.skipFormat||R("Formatted with tofu fmt"),await ne(r,m,"install"),je(m,r,t.json),p.SUCCESS)}async function cr(e,t,r,n,o){w(`Existing manifest detected at ${o} \u2014 running upgrade`);let i=await te(o);w(`Reading files from ${r}`);let a=await he(r,i.platformPrefix);w(`Loading config from ${e}`);let c=await Ie(e);if(t.intentId&&(c.intentId=t.intentId),t.input&&t.input.length>0){let O=Ce(t.input);c.inputs={...c.inputs||{},...O}}let f=Fe(c);w(`Intent ID: ${c.intentId}`),w("Resolving template...");let l={template:await P().resolve(f,{noCache:t.cache===!1,timeout:n}),inputs:c.inputs,intentId:c.intentId,platformPrefix:c.platformPrefix,options:{skipFormat:t.skipFormat??c.options?.skipFormat}},g=await(0,oe.createEngine)().upgrade(l,a,i);return g.sourceBlocked&&!t.allowSourceChange?(k("Source mismatch: current files were generated from a different template source."),it(g.driftReport,t.json),t.json?D(JSON.stringify({blocked:!0,reason:"source_mismatch",sourceBlocked:!0,driftReport:{drift:!g.driftReport.valid,summary:g.driftReport.summary}},null,2)):s("Use --allow-source-change to proceed with source migration."),p.DRIFT_BLOCKED):(g.sourceBlocked&&k("Proceeding with source migration (--allow-source-change)."),it(g.driftReport,t.json),!g.driftReport.valid&&!t.force?(t.json?D(JSON.stringify({blocked:!0,reason:"content_drift",sourceBlocked:g.sourceBlocked,driftReport:{drift:!0,summary:g.driftReport.summary}},null,2)):s("Use --force to proceed despite drift."),p.DRIFT_BLOCKED):(g.driftReport.valid||k("Proceeding despite drift (--force)."),t.dryRun?lr(g,a,t.json):(await ne(r,g,"upgrade"),t.json?D(mr(g)):s(mt(g)),p.SUCCESS)))}async function ur(e,t){let r=[];for(let[n,o]of e){if(n==="manifest.json")continue;let i="";try{i=await(0,U.readFile)((0,N.join)(t,n),"utf-8")}catch{}i!==o.content&&r.push({filename:n,diff:M(i,o.content,n)})}return r}function fr(e,t){let r=[];for(let[n,o]of e){if(n==="manifest.json")continue;let a=t.get(n)?.content??"";a!==o.content&&r.push({filename:n,diff:M(a,o.content,n)})}return r}function lt(e){if(e.length===0){s(`
8
+ No file changes detected.`);return}s(`
9
+ ${e.length} file(s) changed:
10
+ `);for(let t of e)s(t.diff)}function it(e,t){if(t){D(JSON.stringify({drift:!e.valid,summary:e.summary,entries:e.entries.map(n=>({path:n.path,type:n.type,details:n.details}))},null,2));return}if(e.valid){R("No drift detected in managed files.");return}let r=e.summary.driftedFiles+e.summary.missingFiles+e.summary.unexpectedFiles+e.summary.inconsistentFiles;k(`Drift detected in ${r} managed files:`);for(let n of e.entries)s(` ${le(n.type)} ${n.path}`),s(` ${n.details}`);s(""),s("Use --force to override and proceed with upgrade.")}async function Pe(e,t,r){let n=await ur(e.files,t);return r?D(JSON.stringify({dryRun:!0,aggregateHash:e.manifest.aggregateHash,templateSource:e.manifest.templateSource,templateVersion:e.manifest.templateVersion,fileCount:e.files.size,files:Array.from(e.files.entries()).map(([o,i])=>({path:o,size:Buffer.byteLength(i.content,"utf-8")})),changes:n.map(o=>({filename:o.filename,diff:o.diff}))},null,2)):(s("Dry run \u2014 no files written."),s(""),s(`Aggregate Hash: ${e.manifest.aggregateHash}`),s(`Template: ${e.manifest.templateSource}`),s(`Version: ${e.manifest.templateVersion}`),s(`Files: ${e.files.size}`),lt(n)),p.SUCCESS}function je(e,t,r){if(r)D(JSON.stringify({success:!0,aggregateHash:e.manifest.aggregateHash,fileCount:e.files.size,outputDir:t,files:Array.from(e.files.entries()).map(([n,o])=>({path:n,size:Buffer.byteLength(o.content,"utf-8")}))},null,2));else{let n=[];for(let[o,i]of e.files)n.push({path:o,size:Buffer.byteLength(i.content,"utf-8")});s(""),s(`Output written to: ${t}`),s(`Aggregate Hash: ${e.manifest.aggregateHash}`),Ue(n)}}function lr(e,t,r){let n=fr(e.files,t);return r?D(JSON.stringify({dryRun:!0,templateSource:e.manifest.templateSource,templateVersion:e.manifest.templateVersion,aggregateHash:e.manifest.aggregateHash,driftReport:e.driftReport,files:Array.from(e.files.entries()).map(([o,i])=>({path:o,zone:i.zone})),changes:n.map(o=>({filename:o.filename,diff:o.diff}))},null,2)):(s("Dry run \u2014 no files written."),s(""),s(mt(e)),lt(n)),p.SUCCESS}function mt(e){let t=[];t.push(`\u2713 Upgrade complete: ${e.manifest.templateSource} v${e.manifest.templateVersion}`),t.push("");let r=0,n=0;for(let[,o]of e.files)o.zone==="platform"?r++:n++;return t.push(` Updated: ${r} files (Platform Zone)`),t.push(` Unchanged: ${n} files (User Zone)`),t.push(""),t.push(`Aggregate Hash: ${e.manifest.aggregateHash}`),t.join(`
11
+ `)}function mr(e){return JSON.stringify({success:!0,templateSource:e.manifest.templateSource,templateVersion:e.manifest.templateVersion,aggregateHash:e.manifest.aggregateHash,driftReport:e.driftReport,files:Array.from(e.files.entries()).map(([t,r])=>({path:t,zone:r.zone}))},null,2)}var dt=require("commander"),we=require("fs/promises"),ie=require("path"),gt=B(require("handlebars")),ve=require("@truefoundry/tfy-infra-engine");function ht(){return new dt.Command("validate").description("Validate a template (for template authors)").option("-d, --directory <dir>","Path to local template directory",".").option("--strict","Fail on warnings").option("--skip-format","Skip tofu fmt validation").action(async t=>{try{let r=await pr(t);process.exit(r)}catch(r){S(r)}})}async function pt(e){try{return await(0,we.stat)(e),!0}catch{return!1}}async function pr(e){let t=(0,ie.resolve)(e.directory),r=[],n=!1;s(`Validating template at: ${t}`),s("");let o=(0,ie.join)(t,"build","bundle.json"),i=(0,ie.join)(t,"bundle.json"),a=await pt(o)?o:i;if(!await pt(a))return h("bundle.json not found in template directory or build/ subdirectory"),p.VALIDATION_ERROR;w("Checking bundle.json...");try{let u=await(0,we.readFile)(a,"utf-8"),m;try{m=JSON.parse(u)}catch(F){throw new Error(`Invalid JSON: ${F.message}`,{cause:F})}let l=m.metadata;if(!l||typeof l!="object")throw new Error('bundle.json must contain a "metadata" object');if(!l.name||!l.version)throw new Error('bundle.json metadata must contain "name" and "version"');let d=m.jsonSchema;if(!d||typeof d!="object")throw new Error('bundle.json must contain a "jsonSchema" object');(0,ve.validateJsonSchemaStructure)(d),R("bundle.json schema is valid");let g=m.files,O=m.staticFiles,x=Object.keys(g??{}).length,_=Object.keys(O??{}).length;if(x===0&&_===0)r.push({type:"error",message:"Bundle contains no template files"}),n=!0;else{for(let[F,se]of Object.entries(g??{}))try{gt.default.precompile(se),w(` \u2713 ${F}`)}catch(G){r.push({type:"error",message:`Invalid Handlebars syntax: ${G.message}`,file:F}),n=!0}R(`Found ${x} HBS template(s)`+(_>0?` and ${_} static file(s)`:"")+" with valid syntax")}}catch(u){r.push({type:"error",message:u.message,file:"bundle.json"}),n=!0}e.skipFormat||(w("Checking tofu fmt availability..."),await(0,ve.isTofuAvailable)()?R("tofu fmt available"):r.push({type:"warning",message:"tofu not found - format validation skipped"})),s("");let c=r.filter(u=>u.type==="warning"),f=r.filter(u=>u.type==="error");if(c.length>0){k(`${c.length} warning(s):`);for(let u of c){let m=u.file?`${u.file}: `:"";s(` \u2022 ${m}${u.message}`)}s("")}if(f.length>0){h(`${f.length} error(s):`);for(let u of f){let m=u.file?`${u.file}: `:"";s(` \u2022 ${m}${u.message}`)}s("")}return n?(h("Template validation failed"),p.VALIDATION_ERROR):e.strict&&c.length>0?(h("Template validation failed (strict mode)"),p.VALIDATION_ERROR):(R("Template is valid"),p.SUCCESS)}var yt=require("commander"),I=require("fs/promises"),j=require("path"),wt=B(require("js-yaml")),vt=require("@truefoundry/tfy-infra-engine");function Rt(){return new yt.Command("test").description("Run test fixtures against a template").option("-d, --directory <dir>","Path to local template directory",".").option("--fixture <name>","Run specific fixture only").option("--update","Update expected outputs with actual results").option("--diff","Show unified diff on failures").option("--skip-format","Skip tofu fmt in tests").action(async t=>{try{let r=await dr(t);process.exit(r)}catch(r){S(r)}})}async function dr(e){let t=(0,j.resolve)(e.directory),r=(0,j.join)(t,"tests"),n=`file://${t}`;s(`Testing template at: ${t}`),s("");let o;try{let l=await(0,I.readdir)(r);o=[];for(let d of l)try{let g=(0,j.join)(r,d,"inputs.yaml");await(0,I.readFile)(g),o.push(d)}catch{}}catch{return h(`No tests directory found at ${r}`),p.VALIDATION_ERROR}if(o.length===0)return h("No test fixtures found"),s(""),s("Expected structure:"),s(" tests/"),s(" <fixture-name>/"),s(" inputs.yaml"),s(" expected/"),p.VALIDATION_ERROR;if(e.fixture){if(!o.includes(e.fixture))return h(`Fixture '${e.fixture}' not found`),s(`Available fixtures: ${o.join(", ")}`),p.VALIDATION_ERROR;o=[e.fixture]}s(`Running ${o.length} fixture(s)...`),s("");let a=await P().resolve(n,{noCache:!0}),c=(0,vt.createEngine)(),f=[];for(let l of o){let d=(0,j.join)(r,l),g=(0,j.join)(d,"inputs.yaml"),O=(0,j.join)(d,"expected");w(`Running fixture: ${l}`);try{let x=await(0,I.readFile)(g,"utf-8"),_=wt.load(x),F={template:a,inputs:_,intentId:`test-${l}`,options:{skipFormat:e.skipFormat}},se=await c.install(F);if(e.update)await gr(O,se),R(`${l}: updated`),f.push({name:l,passed:!0});else{let{errors:G,diffs:ae}=await hr(O,se,e.diff);if(G.length===0)R(`${l}: passed`),f.push({name:l,passed:!0});else{h(`${l}: failed`);for(let K of G)s(` ${K}`);if(e.diff&&ae&&ae.length>0)for(let K of ae){s(` --- expected/${K.filename}`),s(` +++ actual/${K.filename}`);let Nt=K.diff.split(`
12
+ `).slice(4);for(let _e of Nt)_e&&s(` ${_e}`)}f.push({name:l,passed:!1,errors:G,diffs:ae})}}}catch(x){h(`${l}: error`),s(` ${x.message}`),f.push({name:l,passed:!1,errors:[x.message]})}}s("");let u=f.filter(l=>l.passed).length,m=f.filter(l=>!l.passed).length;return m===0?(R(`All ${u} fixture(s) passed`),p.SUCCESS):(h(`${m} fixture(s) failed, ${u} passed`),p.VALIDATION_ERROR)}function De(e){return e.replace(/("source"\s*:\s*)"file:\/\/[^"]*"/g,'$1"<local>"')}async function gr(e,t){await(0,I.mkdir)(e,{recursive:!0});for(let[r,n]of t.files){if(r==="manifest.json")continue;let o=(0,j.join)(e,r);await(0,I.writeFile)(o,De(n.content),"utf-8")}}async function hr(e,t,r){let n=[],o=[];for(let[i,a]of t.files){if(i==="manifest.json")continue;let c=a.content,f=(0,j.join)(e,i);try{let u=await(0,I.readFile)(f,"utf-8"),m=De(c.trim().replace(/\r\n/g,`
13
+ `)),l=De(u.trim().replace(/\r\n/g,`
14
+ `));if(m!==l&&(n.push(`${i}: content mismatch`),r)){let d=M(l,m,i);o.push({filename:i,diff:d})}}catch{n.push(`${i}: expected file not found`)}}try{let i=await(0,I.readdir)(e);for(let a of i)!t.files.has(a)&&a!=="manifest.json"&&n.push(`${a}: unexpected file in expected/`)}catch{}return{errors:n,diffs:r?o:void 0}}var Ot=require("commander"),v=require("fs/promises"),y=require("path"),ke=B(require("js-yaml")),L=require("@truefoundry/tfy-infra-engine");var Et=B(require("chokidar")),bt=require("path");function yr(e,t){let r=e.replace(/\\/g,"/"),n=(0,bt.basename)(r);return t&&r.endsWith(t.replace(/\\/g,"/"))||n==="bundle.json"||n==="schema-pkg.json"?"schema":/\/src\//.test(r)&&!n.startsWith("_")?"template":n==="inputs.yaml"?"inputs":"other"}function Ne(e,t={}){let{onChange:r,onReady:n,onError:o,ignored:i=/(^|[/\\])\..|(^|[/\\])node_modules($|[/\\])|(^|[/\\])dev($|[/\\])/,debounceMs:a=100,schemaFile:c}=t,f=Et.default.watch(e,{ignored:i,persistent:!0,ignoreInitial:!0,awaitWriteFinish:{stabilityThreshold:a,pollInterval:50}}),u=m=>l=>{let d=yr(l,c);r&&r({type:m,path:l,fileType:d})};return f.on("change",u("change")),f.on("add",u("add")),f.on("unlink",u("unlink")),n&&f.on("ready",n),o&&f.on("error",m=>o(m instanceof Error?m:new Error(String(m)))),f}function Tt(){return new Ot.Command("dev").description("Watch mode for rapid template iteration").option("-d, --directory <dir>","Path to template version directory").option("--schema <path>","Path to schema package JSON (from tfy-schema-tool --schema-package)").option("--src <dir>","Path to template src/ directory").option("--name <string>","Template name (used with --schema mode)").option("--version <semver>","Template version (used with --schema mode)").option("-f, --fixture <path>","Input file or fixture to use for testing").requiredOption("-o, --output <dir>","Output directory for rendered files").option("--no-test","Only validate, do not run tests").action(async t=>{try{let r=await Rr(t);process.exit(r)}catch(r){S(r)}})}async function wr(e){try{await(0,v.access)(e)}catch{return{valid:!1,error:`Directory not found: ${e}`}}let t=(0,y.join)(e,"build","bundle.json");try{return await(0,v.access)(t),{valid:!0}}catch{}let r=(0,y.join)(e,"bundle.json");try{return await(0,v.access)(r),{valid:!0}}catch{return{valid:!1,error:`bundle.json not found in ${e} or ${e}/build`}}}async function St(e,t){let r=(0,y.resolve)(t);try{await(0,v.rm)(r,{recursive:!0,force:!0})}catch{}return await(0,v.mkdir)(r,{recursive:!0}),r}function vr(e){return!!(e.schema&&e.src)}async function Rr(e){return vr(e)?Er(e):Tr(e)}async function Er(e){let t=(0,y.resolve)(e.schema),r=(0,y.resolve)(e.src),n=e.name??"dev-template",o=e.version,i;if(e.fixture){i=(0,y.resolve)(e.fixture);try{await(0,v.access)(i)}catch{return h(`Fixture not found: ${e.fixture}`),p.VALIDATION_ERROR}}else return h("--fixture is required in schema mode"),s("Use -f to specify an inputs.yaml file."),p.VALIDATION_ERROR;let a=await St(".",e.output),c=(0,y.relative)(process.cwd(),a);s(`[schema] ${(0,y.relative)(process.cwd(),t)}`),s(`[src] ${(0,y.relative)(process.cwd(),r)}`),s(`[output] ${c}/`),s(`[fixture] ${(0,y.relative)(process.cwd(),i)}`),s("");let f=(0,L.createEngine)(),u=async()=>{try{let d=await xr(t,r,n,o);await Or(f,d,i,a,e.test)}catch(d){h("[render] \u2717 Render failed"),s(` ${d.message}`)}};await u();let l=Ne([t,r,i],{schemaFile:t,onChange:d=>{(async()=>(s(`[changed] ${(0,y.relative)(process.cwd(),d.path)}`),await u()))()},onReady:()=>{s("[ready] Watching for changes... (Ctrl+C to exit)"),s("")},onError:d=>{h(`Watch error: ${d.message}`)}});return new Promise(d=>{let g=()=>{s(""),s("[stopped] Watch mode ended"),l.close(),d(p.SUCCESS)};process.on("SIGINT",g),process.on("SIGTERM",g)})}function br(e){let t=e.properties;if(!t)return;let r=t.version;if(!r||typeof r=="boolean")return;let n=r.default;return typeof n=="string"?n:void 0}async function xr(e,t,r,n){let o=await(0,v.readFile)(e,"utf-8"),a=JSON.parse(o).jsonSchema;if(!a||typeof a!="object")throw new Error('Schema package must contain a "jsonSchema" object');let c=n??br(a)??"0.0.0-dev",{files:f,staticFiles:u}=await ue(t);return{metadata:{name:r,version:c},jsonSchema:a,files:f,staticFiles:u,source:`file://${t}`,version:{semver:c}}}async function Or(e,t,r,n,o){let i=await(0,v.readFile)(r,"utf-8"),a=ke.load(i),c={template:t,inputs:a,intentId:`dev-${Date.now()}`,options:{skipFormat:!1}},f=await e.install(c),u=Array.from(f.files.keys()).filter(l=>l!=="manifest.json").length;s(`[validate] \u2713 ${u} template files valid`),await(0,v.rm)(n,{recursive:!0,force:!0}),await(0,v.mkdir)(n,{recursive:!0});let m=0;for(let[l,d]of f.files){if(l==="manifest.json")continue;let g=(0,y.join)(n,l);await(0,v.mkdir)((0,y.dirname)(g),{recursive:!0}),await(0,v.writeFile)(g,d.content,"utf-8"),m++}s(`[render] \u2713 ${m} files written to ${(0,y.relative)(process.cwd(),n)}/`),o!==!1&&await Ir(f,n)}async function Tr(e){let t=(0,y.resolve)(e.directory||"."),r=await wr(t);if(!r.valid)return h(r.error),p.VALIDATION_ERROR;let n;if(e.fixture){n=(0,y.resolve)(t,e.fixture);try{await(0,v.access)(n)}catch{return h(`Fixture not found: ${e.fixture}`),p.VALIDATION_ERROR}}else{let u=await H(t);if(!u)return h("No fixture found under tests/ (checked tests/default/ and tests/minimal/)"),s("Either create a fixture or use --fixture to specify one."),p.VALIDATION_ERROR;n=u}let o=await St(t,e.output),i=(0,y.relative)(process.cwd(),o);s(`[watching] ${(0,y.relative)(process.cwd(),t)}`),s(`[output] ${i}/`),s(`[fixture] ${(0,y.relative)(process.cwd(),n)}`),s("");let a=(0,L.createEngine)(),c=`file://${t}`;await xt(a,c,t,n,o,e.test);let f=Ne(t,{onChange:u=>{(async()=>{let m=(0,y.relative)(t,u.path);s(`[changed] ${m}`),u.fileType==="schema"?await It(c):(u.fileType==="template"||u.fileType==="inputs")&&await xt(a,c,t,n,o,e.test)})()},onReady:()=>{s("[ready] Watching for changes... (Ctrl+C to exit)"),s("")},onError:u=>{h(`Watch error: ${u.message}`)}});return new Promise(u=>{let m=()=>{s(""),s("[stopped] Watch mode ended"),f.close(),u(p.SUCCESS)};process.on("SIGINT",m),process.on("SIGTERM",m)})}async function It(e){try{return(await P().resolve(e,{noCache:!0})).jsonSchema?(s("[validate] \u2713 schema valid"),!0):(h("[validate] \u2717 schema invalid"),!1)}catch(t){return h("[validate] \u2717 schema invalid"),s(` ${t.message}`),!1}}async function xt(e,t,r,n,o,i){if(await It(t))try{let c=await(0,v.readFile)(n,"utf-8"),f=ke.load(c),l={template:await P().resolve(t,{noCache:!0}),inputs:f,intentId:`dev-${Date.now()}`,options:{skipFormat:!1}},d=await e.install(l),g=Array.from(d.files.keys()).filter(x=>x!=="manifest.json").length;s(`[validate] \u2713 ${g} template files valid`),await(0,v.rm)(o,{recursive:!0,force:!0}),await(0,v.mkdir)(o,{recursive:!0});let O=0;for(let[x,_]of d.files){if(x==="manifest.json")continue;let F=(0,y.join)(o,x);await(0,v.mkdir)((0,y.dirname)(F),{recursive:!0}),await(0,v.writeFile)(F,_.content,"utf-8"),O++}s(`[render] \u2713 ${O} files written to ${(0,y.relative)(process.cwd(),o)}/`),i!==!1&&await Sr(r,d,o)}catch(c){h("[render] \u2717 Render failed"),s(` ${c.message}`),c instanceof L.EngineError&&c.code===L.EngineErrorCode.INPUT_VALIDATION_FAILED&&c.details?.errors&&s(fe(c.details.errors))}}async function Sr(e,t,r){let n=(0,y.join)(e,"tests","default","expected");await Fr(t,n)}async function Ir(e,t){}async function Fr(e,t){let r=Date.now();try{await(0,v.access)(t)}catch{s("[test] \u26A0 No expected/ directory, skipping comparison");return}let n=[];for(let[i,a]of e.files){if(i==="manifest.json")continue;let c=a.content,f=(0,y.join)(t,i);try{let u=await(0,v.readFile)(f,"utf-8"),m=c.trim().replace(/\r\n/g,`
15
+ `),l=u.trim().replace(/\r\n/g,`
16
+ `);if(m!==l){let d=M(l,m,i);n.push({file:i,diff:d})}}catch{n.push({file:i})}}let o=Date.now()-r;if(n.length===0)s(`[test] \u2713 default: passed (${o}ms)`);else{h("[test] \u2717 default: failed");for(let i of n)if(s(` ${i.file}: content mismatch`),i.diff){let a=i.diff.split(`
17
+ `).slice(4,14);for(let c of a)c&&s(` ${c}`);i.diff.split(`
18
+ `).length>14&&s(" ... (diff truncated)")}}}var Ft=require("commander"),Re=require("path"),Ct=require("@truefoundry/tfy-infra-engine");var Cr=1;function $t(){return new Ft.Command("verify").description("Check integrity of files on disk against a manifest").option("-d, --directory <dir>","Path to cluster directory containing manifest.json",".").option("--json","Output drift report as structured JSON").action(async t=>{try{let r=await $r(t);process.exit(r)}catch(r){S(r)}})}async function $r(e){let t=(0,Re.resolve)(e.directory),r=(0,Re.join)(t,"manifest.json");w(`Loading manifest from ${r}`);let n=await te(r);w(`Reading files from ${t}`);let o=await he(t,n.platformPrefix),a=await(0,Ct.createEngine)().verify(o,n);return e.json?s(jr(a.driftReport)):s(Pr(a.driftReport)),a.driftReport.valid?p.SUCCESS:Cr}function Pr(e){let t=[],r=e.summary.driftedFiles+e.summary.missingFiles+e.summary.unexpectedFiles;if(e.valid)t.push(`\u2713 Verification passed: ${e.summary.totalFiles} files checked, ${r} issues`),t.push(""),t.push(`Aggregate Hash: ${e.aggregateHash.expected}`);else{t.push(`\u2717 Drift detected: ${r} issues found`),t.push("");for(let n of e.entries){let o=le(n.type);t.push(` ${o} ${n.path}`),t.push(` ${n.details}`),t.push("")}t.push(`Summary: ${e.summary.totalFiles} files checked, ${e.summary.driftedFiles} drifted, ${e.summary.missingFiles} missing`+(e.summary.unexpectedFiles>0?`, ${e.summary.unexpectedFiles} unexpected`:""))}return t.join(`
19
+ `)}function jr(e){return JSON.stringify(e,null,2)}function jt(){let e=new Pt.Command("tpl").description("Template operations").enablePositionalOptions();return e.addCommand(Be()),e.addCommand(Tt()),e.addCommand(ut()),e.addCommand(ht()),e.addCommand(Rt()),e.addCommand($t()),e}process.env.INIT_CWD&&process.chdir(process.env.INIT_CWD);var Dr="0.1.4-canary.595d398";async function Nr(){let e=new Dt.Command;e.name("tfy-init").description("TrueFoundry infrastructure templating CLI").enablePositionalOptions().version(Dr,"-v, --version","Show version").option("--verbose","Enable verbose output").option("-q, --quiet","Suppress non-error output").hook("preAction",t=>{let r=t.opts();r.quiet?be("quiet"):r.verbose&&be("verbose")}),e.addCommand(jt()),await e.parseAsync(process.argv)}Nr().catch(e=>{console.error("Fatal error:",e),process.exit(1)});
16
20
  //# sourceMappingURL=index.js.map