@appinit/cli 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +68 -0
- package/README.md +27 -95
- package/dist/index.d.ts +371 -2
- package/dist/index.js +3795 -24
- package/dist/templates/backend-express/.appinit-generate/project/.appinit/project.json +6 -0
- package/dist/templates/backend-express/.appinit-generate/project/.appinit/template.json +6 -0
- package/dist/templates/backend-express/.appinit-generate/project/.env.example +4 -0
- package/dist/templates/backend-express/.appinit-generate/project/.husky/pre-commit +1 -0
- package/dist/templates/backend-express/.appinit-generate/project/.prettierignore +3 -0
- package/dist/templates/backend-express/.appinit-generate/project/.prettierrc +28 -0
- package/dist/templates/backend-express/.appinit-generate/project/.vscode/settings.json +6 -0
- package/dist/templates/backend-express/.appinit-generate/project/Dockerfile +14 -0
- package/dist/templates/backend-express/.appinit-generate/project/README.md +26 -0
- package/dist/templates/backend-express/.appinit-generate/project/eslint.config.js +76 -0
- package/dist/templates/backend-express/.appinit-generate/project/nodemon.json +8 -0
- package/dist/templates/backend-express/.appinit-generate/project/package.json +69 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/__tests__/health.test.ts +19 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/__tests__/validation.test.ts +36 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/app.ts +72 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/config/defaults.ts +5 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/config/dev.ts +3 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/config/env.ts +62 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/config/prod.ts +3 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/config/test.ts +3 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/errors/app-error.ts +20 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/errors/not-found.ts +16 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/middleware/error-handler.ts +73 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/middleware/http-logger.ts +5 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/middleware/validate.ts +17 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/routes/health.ts +13 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/routes/users.ts +12 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/schema/create-user.schema.ts +8 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/server.ts +54 -0
- package/dist/templates/backend-express/.appinit-generate/project/src/utils/logger.ts +21 -0
- package/dist/templates/backend-express/.appinit-generate/project/tsconfig.json +30 -0
- package/dist/templates/backend-express/.appinit-generate/project/vitest.config.ts +15 -0
- package/dist/templates/backend-express/.appinit-preview/project/.appinit/project.json +6 -0
- package/dist/templates/backend-express/.appinit-preview/project/.appinit/template.json +6 -0
- package/dist/templates/backend-express/.appinit-preview/project/.env.example +4 -0
- package/dist/templates/backend-express/.appinit-preview/project/.husky/pre-commit +1 -0
- package/dist/templates/backend-express/.appinit-preview/project/.prettierignore +3 -0
- package/dist/templates/backend-express/.appinit-preview/project/.prettierrc +28 -0
- package/dist/templates/backend-express/.appinit-preview/project/.vscode/settings.json +6 -0
- package/dist/templates/backend-express/.appinit-preview/project/Dockerfile +14 -0
- package/dist/templates/backend-express/.appinit-preview/project/README.md +26 -0
- package/dist/templates/backend-express/.appinit-preview/project/eslint.config.js +76 -0
- package/dist/templates/backend-express/.appinit-preview/project/nodemon.json +8 -0
- package/dist/templates/backend-express/.appinit-preview/project/package.json +69 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/__tests__/health.test.ts +19 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/__tests__/validation.test.ts +36 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/app.ts +72 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/config/defaults.ts +5 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/config/dev.ts +3 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/config/env.ts +62 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/config/prod.ts +3 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/config/test.ts +3 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/errors/app-error.ts +20 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/errors/not-found.ts +16 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/middleware/error-handler.ts +73 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/middleware/http-logger.ts +5 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/middleware/validate.ts +17 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/routes/health.ts +13 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/routes/users.ts +12 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/schema/create-user.schema.ts +8 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/server.ts +54 -0
- package/dist/templates/backend-express/.appinit-preview/project/src/utils/logger.ts +21 -0
- package/dist/templates/backend-express/.appinit-preview/project/tsconfig.json +30 -0
- package/dist/templates/backend-express/.appinit-preview/project/vitest.config.ts +15 -0
- package/dist/templates/backend-express/README.md +107 -0
- package/dist/templates/backend-express/appinit.template.json +25 -0
- package/dist/templates/backend-express/docs/README.md +11 -0
- package/dist/templates/backend-express/package.json +5 -0
- package/dist/templates/backend-express/template/.env.example +4 -0
- package/dist/templates/backend-express/template/.husky/pre-commit +1 -0
- package/dist/templates/backend-express/template/.prettierignore +3 -0
- package/dist/templates/backend-express/template/.prettierrc +28 -0
- package/dist/templates/backend-express/template/.vscode/settings.json +6 -0
- package/dist/templates/backend-express/template/Dockerfile +14 -0
- package/dist/templates/backend-express/template/README.md__tmpl +26 -0
- package/dist/templates/backend-express/template/eslint.config.js +76 -0
- package/dist/templates/backend-express/template/nodemon.json +8 -0
- package/dist/templates/backend-express/template/package.json__tmpl +69 -0
- package/dist/templates/backend-express/template/src/__tests__/health.test.ts +19 -0
- package/dist/templates/backend-express/template/src/__tests__/validation.test.ts +36 -0
- package/dist/templates/backend-express/template/src/app.ts +72 -0
- package/dist/templates/backend-express/template/src/config/defaults.ts +5 -0
- package/dist/templates/backend-express/template/src/config/dev.ts +3 -0
- package/dist/templates/backend-express/template/src/config/env.ts +62 -0
- package/dist/templates/backend-express/template/src/config/prod.ts +3 -0
- package/dist/templates/backend-express/template/src/config/test.ts +3 -0
- package/dist/templates/backend-express/template/src/errors/app-error.ts +20 -0
- package/dist/templates/backend-express/template/src/errors/not-found.ts +16 -0
- package/dist/templates/backend-express/template/src/middleware/error-handler.ts +73 -0
- package/dist/templates/backend-express/template/src/middleware/http-logger.ts +5 -0
- package/dist/templates/backend-express/template/src/middleware/validate.ts +17 -0
- package/dist/templates/backend-express/template/src/routes/health.ts +13 -0
- package/dist/templates/backend-express/template/src/routes/users.ts +12 -0
- package/dist/templates/backend-express/template/src/schema/create-user.schema.ts +8 -0
- package/dist/templates/backend-express/template/src/server.ts +54 -0
- package/dist/templates/backend-express/template/src/utils/logger.ts +21 -0
- package/dist/templates/backend-express/template/tsconfig.json +30 -0
- package/dist/templates/backend-express/template/vitest.config.ts +15 -0
- package/dist/templates/backend-express/template.config.ts +36 -0
- package/dist/templates/backend-express/variables/defaults.json +7 -0
- package/dist/templates/backend-express/variables/defaults.ts +4 -0
- package/dist/templates/backend-express/variables/schema.json +22 -0
- package/dist/templates/backend-express/variables/schema.ts +11 -0
- package/dist/templates/backend-express/variables/transform.ts +6 -0
- package/dist/templates/backend-fastify/.appinit-generate/project/.appinit/project.json +6 -0
- package/dist/templates/backend-fastify/.appinit-generate/project/.appinit/template.json +6 -0
- package/dist/templates/backend-fastify/.appinit-generate/project/README.md +26 -0
- package/dist/templates/backend-fastify/.appinit-generate/project/nodemon.json +8 -0
- package/dist/templates/backend-fastify/.appinit-generate/project/package-lock.json +3245 -0
- package/dist/templates/backend-fastify/.appinit-generate/project/package.json +24 -0
- package/dist/templates/backend-fastify/.appinit-generate/project/src/app.ts +13 -0
- package/dist/templates/backend-fastify/.appinit-generate/project/src/server.ts +15 -0
- package/dist/templates/backend-fastify/.appinit-generate/project/tsconfig.json +30 -0
- package/dist/templates/backend-fastify/README.md +27 -0
- package/dist/templates/backend-fastify/appinit.template.json +25 -0
- package/dist/templates/backend-fastify/docs/README.md +12 -0
- package/dist/templates/backend-fastify/package.json +5 -0
- package/dist/templates/backend-fastify/template/README.md__tmpl +26 -0
- package/dist/templates/backend-fastify/template/nodemon.json +8 -0
- package/dist/templates/backend-fastify/template/package.json__tmpl +24 -0
- package/dist/templates/backend-fastify/template/src/app.ts +13 -0
- package/dist/templates/backend-fastify/template/src/server.ts +15 -0
- package/dist/templates/backend-fastify/template/tsconfig.json +30 -0
- package/dist/templates/backend-fastify/template.config.ts +32 -0
- package/dist/templates/backend-fastify/variables/defaults.json +4 -0
- package/dist/templates/backend-fastify/variables/defaults.ts +3 -0
- package/dist/templates/backend-fastify/variables/schema.json +7 -0
- package/dist/templates/backend-fastify/variables/schema.ts +1 -0
- package/dist/templates/backend-fastify/variables/transform.ts +3 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/.appinit/project.json +6 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/.appinit/template.json +6 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/README.md +33 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/app/favicon.ico +0 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/app/globals.css +42 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/app/layout.tsx +33 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/app/page.module.css +141 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/app/page.tsx +67 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/next-env.d.ts +6 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/next.config.ts +8 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/package.json +25 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/public/file.svg +1 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/public/globe.svg +1 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/public/next.svg +1 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/public/vercel.svg +1 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/public/window.svg +1 -0
- package/dist/templates/frontend-nextjs/.appinit-generate/project/tsconfig.json +34 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/.appinit/project.json +6 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/.appinit/template.json +6 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/README.md +33 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/app/favicon.ico +0 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/app/globals.css +42 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/app/layout.tsx +33 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/app/page.module.css +141 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/app/page.tsx +67 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/next-env.d.ts +6 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/next.config.ts +8 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/package.json +25 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/public/file.svg +1 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/public/globe.svg +1 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/public/next.svg +1 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/public/vercel.svg +1 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/public/window.svg +1 -0
- package/dist/templates/frontend-nextjs/.appinit-preview/project/tsconfig.json +34 -0
- package/dist/templates/frontend-nextjs/README.md +27 -0
- package/dist/templates/frontend-nextjs/appinit.template.json +25 -0
- package/dist/templates/frontend-nextjs/docs/README.md +12 -0
- package/dist/templates/frontend-nextjs/package.json +5 -0
- package/dist/templates/frontend-nextjs/template/README.md__tmpl +33 -0
- package/dist/templates/frontend-nextjs/template/app/favicon.ico +0 -0
- package/dist/templates/frontend-nextjs/template/app/globals.css +42 -0
- package/dist/templates/frontend-nextjs/template/app/layout.tsx +33 -0
- package/dist/templates/frontend-nextjs/template/app/page.module.css +141 -0
- package/dist/templates/frontend-nextjs/template/app/page.tsx +67 -0
- package/dist/templates/frontend-nextjs/template/next-env.d.ts +6 -0
- package/dist/templates/frontend-nextjs/template/next.config.ts +8 -0
- package/dist/templates/frontend-nextjs/template/package.json__tmpl +25 -0
- package/dist/templates/frontend-nextjs/template/public/file.svg +1 -0
- package/dist/templates/frontend-nextjs/template/public/globe.svg +1 -0
- package/dist/templates/frontend-nextjs/template/public/next.svg +1 -0
- package/dist/templates/frontend-nextjs/template/public/vercel.svg +1 -0
- package/dist/templates/frontend-nextjs/template/public/window.svg +1 -0
- package/dist/templates/frontend-nextjs/template/tsconfig.json +34 -0
- package/dist/templates/frontend-nextjs/template.config.ts +28 -0
- package/dist/templates/frontend-nextjs/variables/defaults.json +6 -0
- package/dist/templates/frontend-nextjs/variables/defaults.ts +4 -0
- package/dist/templates/frontend-nextjs/variables/schema.json +22 -0
- package/dist/templates/frontend-nextjs/variables/schema.ts +7 -0
- package/dist/templates/frontend-nextjs/variables/transform.ts +3 -0
- package/dist/templates/frontend-react/.appinit-generate/project/.appinit/project.json +6 -0
- package/dist/templates/frontend-react/.appinit-generate/project/.appinit/template.json +6 -0
- package/dist/templates/frontend-react/.appinit-generate/project/README.md +14 -0
- package/dist/templates/frontend-react/.appinit-generate/project/index.html +12 -0
- package/dist/templates/frontend-react/.appinit-generate/project/package.json +26 -0
- package/dist/templates/frontend-react/.appinit-generate/project/public/vite.svg +30 -0
- package/dist/templates/frontend-react/.appinit-generate/project/src/App.tsx +16 -0
- package/dist/templates/frontend-react/.appinit-generate/project/src/assets/react.svg +1 -0
- package/dist/templates/frontend-react/.appinit-generate/project/src/index.css +9 -0
- package/dist/templates/frontend-react/.appinit-generate/project/src/main.tsx +10 -0
- package/dist/templates/frontend-react/.appinit-generate/project/tsconfig.app.json +31 -0
- package/dist/templates/frontend-react/.appinit-generate/project/tsconfig.json +7 -0
- package/dist/templates/frontend-react/.appinit-generate/project/tsconfig.node.json +26 -0
- package/dist/templates/frontend-react/.appinit-generate/project/vite.config.ts +16 -0
- package/dist/templates/frontend-react/appinit.template.json +26 -0
- package/dist/templates/frontend-react/docs/README.md +12 -0
- package/dist/templates/frontend-react/hooks/afterRender.ts +3 -0
- package/dist/templates/frontend-react/hooks/afterWrite.ts +3 -0
- package/dist/templates/frontend-react/hooks/beforeRender.ts +3 -0
- package/dist/templates/frontend-react/hooks/beforeWrite.ts +3 -0
- package/dist/templates/frontend-react/package.json +16 -0
- package/dist/templates/frontend-react/template/.env.example__tmpl +1 -0
- package/dist/templates/frontend-react/template/README.md__tmpl +14 -0
- package/dist/templates/frontend-react/template/_gitignore +41 -0
- package/dist/templates/frontend-react/template/index.html__tmpl +12 -0
- package/dist/templates/frontend-react/template/package.json__tmpl +26 -0
- package/dist/templates/frontend-react/template/public/vite.svg +30 -0
- package/dist/templates/frontend-react/template/src/App.tsx__tmpl +16 -0
- package/dist/templates/frontend-react/template/src/assets/react.svg +1 -0
- package/dist/templates/frontend-react/template/src/index.css__tmpl +9 -0
- package/dist/templates/frontend-react/template/src/main.tsx__tmpl +10 -0
- package/dist/templates/frontend-react/template/tsconfig.app.json__tmpl +31 -0
- package/dist/templates/frontend-react/template/tsconfig.json__tmpl +7 -0
- package/dist/templates/frontend-react/template/tsconfig.node.json__tmpl +26 -0
- package/dist/templates/frontend-react/template/vite.config.ts__tmpl +16 -0
- package/dist/templates/frontend-react/template.config.ts +26 -0
- package/dist/templates/frontend-react/template.meta.json +10 -0
- package/dist/templates/frontend-react/variables/defaults.json +6 -0
- package/dist/templates/frontend-react/variables/defaults.ts +6 -0
- package/dist/templates/frontend-react/variables/schema.json +15 -0
- package/dist/templates/frontend-react/variables/schema.ts +17 -0
- package/dist/templates/frontend-react/variables/transform.ts +10 -0
- package/dist/templates/frontend-vue/README.md +27 -0
- package/dist/templates/frontend-vue/appinit.template.json +25 -0
- package/dist/templates/frontend-vue/docs/README.md +12 -0
- package/dist/templates/frontend-vue/package.json +5 -0
- package/dist/templates/frontend-vue/template/README.md__tmpl +31 -0
- package/dist/templates/frontend-vue/template/index.html +13 -0
- package/dist/templates/frontend-vue/template/package.json__tmpl +25 -0
- package/dist/templates/frontend-vue/template/public/vite.svg +1 -0
- package/dist/templates/frontend-vue/template/src/App.vue +30 -0
- package/dist/templates/frontend-vue/template/src/assets/vue.svg +1 -0
- package/dist/templates/frontend-vue/template/src/components/HelloWorld.vue +42 -0
- package/dist/templates/frontend-vue/template/src/main.ts +6 -0
- package/dist/templates/frontend-vue/template/src/style.css +79 -0
- package/dist/templates/frontend-vue/template/tsconfig.app.json +16 -0
- package/dist/templates/frontend-vue/template/tsconfig.json +7 -0
- package/dist/templates/frontend-vue/template/tsconfig.node.json +26 -0
- package/dist/templates/frontend-vue/template/vite.config.ts +7 -0
- package/dist/templates/frontend-vue/template.config.ts +32 -0
- package/dist/templates/frontend-vue/variables/defaults.json +6 -0
- package/dist/templates/frontend-vue/variables/defaults.ts +3 -0
- package/dist/templates/frontend-vue/variables/schema.json +22 -0
- package/dist/templates/frontend-vue/variables/schema.ts +1 -0
- package/dist/templates/frontend-vue/variables/transform.ts +3 -0
- package/package.json +40 -29
- package/bin/appinit.js +0 -15
package/dist/index.js
CHANGED
|
@@ -1,25 +1,3796 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`)),{title:o,step(r){return n(r)},complete(r){console.log(l.success(`\u2714 ${r??o}`));},error(r){console.log(l.danger(`\u2716 ${r??o}`));}}}return {group:a,step:n}}var X={version:"1.1.0"};function ee(){return ((process.argv[1]??"").split(/[/\\]/).pop()??"").replace(/\.(js|ts|mjs|cjs)$/i,"")||"appinit"}function w(){return X?.version}async function te(e){let t=process.cwd(),n=x(rt.homedir(),".appinit/cache");return {cwd:t,cacheDir:n,command:e.name,flags:e.flags,args:e.args,cliName:ee(),cliVersion:process.env.APPINIT_CLI_VERSION??w(),runtime:"cli",outputMode:e.flags.json?"json":"text",interactive:!e.flags.nonInteractive,logger:v,spinner:void 0,task:{createManager:()=>Z(e)}}}var oe=["help","version","nonInteractive","skipInstall","skipGit","skipDefaultPacks","dryRun","force","quiet"],ae=["template","projectType"],re={"non-interactive":"nonInteractive","skip-install":"skipInstall","skip-git":"skipGit","project-type":"projectType","skip-default-packs":"skipDefaultPacks","dry-run":"dryRun"},ne={help:"help",h:"help",version:"version",v:"version",json:"json",f:"force",q:"quiet","non-interactive":"nonInteractive","skip-install":"skipInstall","skip-git":"skipGit","project-type":"projectType","dry-run":"dryRun"},I={quiet:false,verbose:false,debug:false,nonInteractive:false,skipInstall:false,skipGit:false,json:"",projectType:"frontend",skipDefaultPacks:false,dryRun:false,force:false};function ie(e){let t=e.splice(2),n=nt(t,{boolean:oe.map(r=>r.toLowerCase()),string:ae.map(r=>r.toLowerCase()),default:I}),a=String(n._?.[0]??"help"),o=ot(n);return {name:a,args:t,flags:o}}function ot(e){let t={...I};for(let[n,a]of Object.entries(e)){if(n==="_"||typeof n!="string")continue;let o=at(n);if(o){if(n.startsWith("no-")){t[o]=false;continue}t[o]=a;}}return t}function at(e){if(!e)return "";if(e=e.replace(/^--?/,"").toLowerCase(),ne[e])return ne[e];if(e in re)return re[e];let t=O(e);return t in I||oe.includes(t)||ae.includes(t)?t:""}function se(e){isCancel(e)&&(cancel("Operation cancelled."),process.exit(1));}async function le(e){let t=await text({message:e.message,initialValue:e.initial});return isCancel(t)&&(cancel("Operation cancelled."),process.exit(1)),t}async function ce(e,t={}){let a=(typeof e.choices=="function"?await e.choices(t):e.choices??[]).map(r=>({label:r.label??r.title??String(r.value),value:r.value,hint:r.hint})),o=await select({...e,initialValue:e.initialValue,options:a});return isCancel(o)&&(cancel("Operation cancelled."),process.exit(1)),o}async function pe(e){let t=await confirm({...e,message:e.message});return se(t),t}async function ue(e,t={}){let a=(typeof e.choices=="function"?await e.choices(t):e.choices??[]).map(r=>({label:r.label??r.title??String(r.value),value:r.value,hint:r.hint})),o=await multiselect({...e,options:a});return se(o),o}async function f(e,t={},n){let a={...t};for(let o of e){if(typeof o.when=="function"&&!await o.when(a))continue;let r;switch(o.type){case "text":r=await le(o);break;case "select":r=await ce(o,a);break;case "confirm":case "toggle":r=await pe(o);break;case "multiselect":r=await ue(o,a);break;default:throw new Error(`Unsupported prompt type "${o.type}`)}if(typeof o.format=="function"&&(r=o.format(r)),typeof o.validate=="function"){let i=await o.validate(r);if(i!==true)throw new Error(`Validation failed for "${o.name}": ${i}`)}a[o.name]=r;}return a}var me=[{label:"React",value:"react",hint:"**The most popular library.** Component-based UI with a huge ecosystem (often paired with Vite or Next.js)."},{label:"Next.js",value:"next",hint:"**Production-ready full-stack React framework.** Excellent for SSR, routing, and APIs."},{label:"Vue",value:"vue",hint:"**Progressive and approachable framework.** Fully-featured with great official libraries (Vue Router, Pinia)."},{label:"Svelte",value:"svelte",hint:"**The compiler.** No virtual DOM. Generates highly optimized vanilla JS for faster runtime performance."},{label:"Angular",value:"angular",hint:"**Opinionated, full-featured framework.** Best for large-scale enterprise applications with built-in tooling."},{label:"SolidJS",value:"solid",hint:"**Fine-grained reactivity.** High performance using compiled JSX and actual DOM nodes, no Virtual DOM."},{label:"Qwik",value:"qwik",hint:"Focuses on **Resumability** to achieve near-instant loading times by lazy loading and resuming application state."},{label:"Astro",value:"astro",hint:"An **all-in-one framework** for building fast, content-focused websites using 'Islands of Interactivity'."},{label:"Framer Motion",value:"framer",hint:"Used primarily for **animation and interactive design.** Not a general-purpose framework, but a powerful animation library."},{label:"React Router",value:"react-router",hint:"A declarative routing library for React (often associated with **Remix** full-stack framework)."},{label:"None",value:"none",hint:"**Vanilla JavaScript/TypeScript.** No framework or library required."}];var de=[{label:"Next.js",value:"next",hint:"**The leading React full-stack framework.** Excellent for server-side rendering (SSR), static site generation (SSG), and API routes."},{label:"Remix",value:"react-router",hint:"**Full-stack React framework built on web standards.** Focuses on route-level data fetching and seamless integration with React Router."},{label:"SvelteKit",value:"svelte",hint:"**The full-stack Svelte framework.** Extremely fast, uses a compiler approach, and offers flexible adapters for deployment."},{label:"Nuxt (Vue)",value:"vue",hint:"**The full-stack Vue framework.** Rich feature set including file-system routing, auto-imports, and powerful module ecosystem."}];var fe=[{label:"Express",value:"express",hint:"**Most popular Node backend.** Simple routing, middleware based, huge ecosystem."},{label:"Koa",value:"koa",hint:"A **minimalist framework** designed by the Express team. Smaller footprint, uses `async/await` for cleaner middleware flow and better error handling."},{label:"Fastify",value:"fastify",hint:"**High-performance & plugin-based.** Best for APIs requiring throughput."},{label:"NestJS",value:"nest",hint:"**Enterprise-grade architecture.** Built-in DI, modules, and scalable structure."},{label:"AdonisJS",value:"adonis",hint:"A **full-featured, opinionated MVC framework** inspired by Laravel/Rails. Comes with its own ORM, CLI, and authentication built-in."},{label:"Hono (Edge-optimized)",value:"hono",hint:"**Tiny + ultra fast for edge runtimes.** Perfect for Cloudflare, Deno, Bun."},{label:"Elysia (Bun)",value:"elysia",hint:"**Deep Bun integration.** Modern type-safe router + middleware."},{label:"None",value:"none",hint:"Choose this if you intend to use a different framework or build your server from scratch."}],ge=[{label:"REST",value:"rest",hint:"**Standard + universal.** Works everywhere and easy to scale gradually."},{label:"tRPC",value:"trpc",hint:"**Full end-to-end types.** Great with React/Next fullstack workflows."},{label:"GraphQL",value:"graphql",hint:"**Flexible querying.** Best for complex relational or mobile apps."}],he=[{label:"Monolith",value:"monolith",hint:"**Single, unified unit.** All components (UI, logic, data) are in one codebase and deployed together. **Simple to develop initially**."},{label:"Modular",value:"modular",hint:"A Monolith where code is **logically separated** into independent modules (features/domains) within the *same* codebase. Easier to maintain than a traditional Monolith."},{label:"Microservices",value:"microservices",hint:"**Independent, specialized services.** Each feature runs as its own separate application with its own database, communicating via APIs. **Highly scalable and fault-tolerant**."},{label:"None",value:"none",hint:"No default architecture"}];var ye={name:"backend",priority:40,condition:(e,t)=>["next","framer","nuxt"].includes(t.frontendFramework)?false:["backend","fullstack"].includes(t.projectType??""),handler:async(e,t,n)=>{if(!e.interactive){let r=e.savedConfig??{};return {backendFramework:n.backendFramework??r.backendFramework??"express",apiStyle:n.apiStyle??r.apiStyle??"rest",backendMode:n.backendMode??r.backendMode??"none"}}return await f([{type:"select",name:"backendFramework",message:"\u{1F9F1} Backend Framework:",choices:fe,initialValue:n.backendFramework??"express"},{type:"select",name:"apiStyle",message:"\u{1F50C} API Style:",choices:ge,initialValue:n.apiStyle??"rest"},{type:"select",name:"backendMode",message:"\u2699\uFE0F Backend Architecture Mode:",choices:he,initialValue:n.backendMode??"none"}],n)}};var be={name:"environment",priority:55,handler:async(e,t,n)=>{let a=e.interactive,o={packageManager:n.packageManager??"pnpm",workspace:n.workspace??"single"};return a?await f([{type:"select",name:"packageManager",message:"\u{1F4E6} Package manager:",choices:pt,initial:n.packageManager??"pnpm"},{type:"select",name:"workspace",message:"\u{1F9E9} Workspace type:",choices:ut,initial:n.workspace??"single"}],n):o}},pt=[{label:"npm",value:"npm",hint:"The **default and most widely-adopted** package manager for Node.js. Great for simplicity and maximum package compatibility."},{label:"pnpm",value:"pnpm",hint:"Focuses on **disk-space efficiency and speed** using content-addressable storage (hard links). Excellent for monorepos."},{label:"yarn",value:"yarn",hint:"Known for **speed, reliability, and monorepo support** (workspaces). Yarn 2+ introduced the Plug'n'Play (PnP) architecture."},{label:"bun",value:"bun",hint:"A **blazingly fast** all-in-one JavaScript runtime, bundler, and package manager. Best for performance-critical or new projects."}],ut=[{label:"Single project",value:"single",hint:"A **traditional project structure** where all code lives in one package. Simple and easy to manage for small-to-medium applications."},{label:"Turborepo (monorepo)",value:"turborepo",hint:"A fast, lightweight build system by Vercel for monorepos. Focuses on **caching and remote computation**."},{label:"Nx (monorepo)",value:"nx",hint:"A **powerful, full-featured monorepo toolkit** that uses a deep graph to understand project dependencies for faster builds and better tooling."},{label:"Lerna (monorepo)",value:"lerna",hint:"A classic tool to manage JavaScript monorepos. Best for **publishing multiple packages** from a single repository."}];var ve={name:"framework",priority:2,condition:(e,t)=>t.projectType==="frontend"||t.projectType==="fullstack",handler:async(e,t,n)=>{let a=e.interactive,o=n.projectType,r=n.frontendFramework??t.frontendFramework??"react",i=o==="fullstack";return a?{frontendFramework:(await f([{type:"select",name:"frontendFramework",message:`\u2699\uFE0F ${i?"Fullstack":"Frontend"} Framework:`,choices:i?de:me,initial:r??(i?"next":"react")}],n)).frontendFramework}:{frontendFramework:r}}};var we=[{label:"MIT",value:"MIT",hint:"A short, permissive license with conditions only requiring preservation of copyright and license notices. Great for maximum compatibility."},{label:"Apache-2.0",value:"Apache-2.0",hint:"A permissive license similar to MIT, but providing an express grant of patent rights from contributors to users."},{label:"GPL-3.0",value:"GPL-3.0",hint:"A strong copyleft license. If you modify or redistribute code under this license, you must also release your source code under GPL-3.0."},{label:"Unlicense",value:"Unlicense",hint:"Dedicates the work to the public domain, offering the fewest restrictions possible."},{label:"Other",value:"Other",hint:"Choose this if your license is not listed above and you will provide details later."}];var ke=[{label:"Frontend Application",value:"frontend",hint:"**A pure client-side application (SPA).** Good for UIs, dashboards, and static sites that consume an existing external API."},{label:"Backend API",value:"backend",hint:"A **server-only project** dedicated to data, business logic, and API endpoints (e.g., using Node.js, Python, or Go)."},{label:"Fullstack (UI + API)",value:"fullstack",hint:"**The modern monolith.** A single project containing both the user interface and the API/server logic (e.g., Next.js, SvelteKit)."},{label:"Library / SDK",value:"library",hint:"A **reusable package** of code meant to be published to NPM and used by other applications (e.g., component library, utility functions)."},{label:"CLI Tool",value:"cli",hint:"A **command-line application** that runs in the terminal. Excellent for automation, deployment, or scaffold generation."}];var j=e=>String(e).trim().toLowerCase().replace(/\s+/g,"-").replace(/[^a-z0-9-_]/g,"").replace(/^-+|-+$/g,"");var h=(...e)=>e.find(t=>t!=null&&t!=="");function xe(e,t,n){let a=n.projectType??t.projectType;return a==="library"||a==="cli"||U(t?.workspaceTool)}var P=e=>{if(typeof e!="string")return null;let t=e.trim().replace(/^@/,"");return t?`@${t}`:null};function Se(e){if(!e||typeof e!="string")return "Name cannot be empty";let t=e.trim();if(!t.length)return "Name cannot be empty";if(/\s/.test(t))return "Spaces are not allowed \u2014 use dash or underscore";if(/[A-Z]/.test(t))return "Use lowercase letters only (npm package rule)";if(/[~'!()*]/.test(t))return "Name contains forbidden characters ~ ' ! ( ) *";if(/[<>:\"\\|?*]/.test(t))return 'Name contains Windows-forbidden characters < > : " \\ | ? *';if(/[/.]$/.test(t))return "Name cannot end with a dot or slash";if(t.startsWith("@")){if(!/^@[a-z0-9\-_]+\/[a-z0-9\-_]+$/.test(t))return "Invalid scoped package. Use @scope/name"}else if(!/^[a-z0-9\-_]+$/.test(t))return "Use only letters, numbers, dash (-), and underscore (_)";return ["node_modules","favicon","core","con","aux","nul","prn"].includes(t.toLowerCase())?`"${t}" is a reserved name`:true}var Pe={name:"meta",priority:1,before(e){console.log("I am meta pack");},handler:async(e,t,n)=>{let a=e.savedConfig??{},o=e.interactive,r={projectName:h(t.projectName,n.projectName,a.projectName,"appinit-app"),projectType:h(t.projectType,n.projectType,a.projectType,"frontend"),description:h(t.description,n.description,a.description,""),licenseType:h(t.licenseType,n.licenseType,a.licenseType,"MIT"),author:h(t.author,n.author,a.author,"appinit"),packageScope:P(h(t.packageScope,n.packageScope,a.packageScope,"@"))};if(!o)return r;let i=[];(!t.projectName||t.projectName.trim()==="")&&i.push({type:"text",name:"projectName",message:"\u{1F9F1} Project name:",initial:a.projectName??"my-appinit-app",format:s=>s?j(s):null,validate:s=>Se(j(s||""))}),i.push({type:"text",name:"description",message:"\u{1F4DD} Short description:",initial:r.description,defaultValue:""},{type:"text",name:"author",message:"\u{1F464} Author (name/email):",initial:r.author,defaultValue:"appinit"},{type:"select",name:"licenseType",message:"\u{1F4DC} License:",choices:we,initial:r.licenseType},{type:"select",name:"projectType",message:"\u{1F4E6} Project type:",choices:ke,initial:r.projectType}),xe(e,t,n)&&i.push({type:"text",name:"packageScope",message:"\u{1F4E6} Package scope (optional, without @):",initial:a.packageScope?.replace?.(/^@/,"")??"",format:s=>s?P(s):null});let c=await f(i,n);return c.packageScope&&(c.packageScope=P(c.packageScope)),{...r,...c}}};var mt=[Pe,ve,be,ye];async function Ce(e,t){let n=e.args[1],a=e.logger,o=[],r={},i={projectName:n,projectType:e.flags.projectType};e.flags.skipDefaultPacks||(o=[...mt]),Array.isArray(t)&&(o=[...o,...t]),o.sort((c,s)=>(c.priority??100)-(s.priority??100)),Object.freeze(o),i.hooks?.beforeAll&&await i.hooks.beforeAll(i,r);for(let c of o)try{if(c.condition&&!await c.condition(i,r)){a.info(`\u23ED\uFE0F Skipping pack: ${c.name}`);continue}i.hooks?.beforeEach&&await i.hooks.beforeEach(c,i,r),c.before&&await c.before(e,i,r);let s=await c.handler(e,i,r);console.log("PACK-",r,c.name,s),s&&typeof s=="object"&&Object.assign(r,s),c.after&&await c.after(e,i,r),i.hooks?.afterEach&&await i.hooks.afterEach(c,i,r);}catch(s){throw a.error(`\u274C Pack failed: "${c.name}"`),a.error(`Reason: ${s.message}`),s}return i.hooks?.afterAll&&await i.hooks.afterAll(i,r),r}var Ae={name:"create",description:"Create new AppInit project",async run(e){let {args:t,task:n,logger:a}=e,o=t[1]??"";n.createManager();await Y({version:w()});let i;try{i=await Ce(e);}catch(je){a?.error("\u274C Prompt engine failed"),a?.error(String(je)),process.exit(1);}console.log("An",i),e.answers=i;let c=i.projectName,s=x(e.cwd,c),m=H(s);m&&(a?.error(`\u274C Invalid target directory: ${s}`),a?.error(`Reason: ${m}`),process.exit(1)),console.log("COnfig",e),W(o);}};var Te={name:"help",description:"",async run(e){return q(w())}};var Re={create:Ae,help:Te};async function Ie(e,t){let n=Re[e];return n||(v.error(`Unknown command: ${e}`),process.exit(1)),n.run(t)}function Ne(e){return e?["init","new"].includes(e)?"create":e.toLowerCase():"help"}async function Ee(e){let t=ie(e),n=Ne(t.name),a=await te(t);try{await Ie(n,a);}catch(o){v.error(`CLI command failed, ${o}`),process.exitCode=1;}}function To(e){Ee(e);}export{To as start};
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { stdin, stdout } from "process";
|
|
5
|
+
import { createInterface } from "readline/promises";
|
|
6
|
+
|
|
7
|
+
// src/args.ts
|
|
8
|
+
function parseVariable(value) {
|
|
9
|
+
const separator = value.indexOf("=");
|
|
10
|
+
if (separator <= 0) throw new Error(`Invalid --var "${value}". Expected key=value.`);
|
|
11
|
+
const key = value.slice(0, separator).trim();
|
|
12
|
+
const raw = value.slice(separator + 1);
|
|
13
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_.-]*$/.test(key)) throw new Error(`Invalid variable name "${key}".`);
|
|
14
|
+
let parsed = raw;
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(raw);
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
return [key, parsed];
|
|
20
|
+
}
|
|
21
|
+
function parseCreateArguments(args) {
|
|
22
|
+
const positional = [];
|
|
23
|
+
const variables = {};
|
|
24
|
+
let dryRun = false;
|
|
25
|
+
let yes = false;
|
|
26
|
+
let template;
|
|
27
|
+
let templateDirectory;
|
|
28
|
+
let install = false;
|
|
29
|
+
let git = false;
|
|
30
|
+
let packageManager;
|
|
31
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
32
|
+
const argument = args[index];
|
|
33
|
+
if (argument === "--dry-run") dryRun = true;
|
|
34
|
+
else if (argument === "--install") install = true;
|
|
35
|
+
else if (argument === "--git") git = true;
|
|
36
|
+
else if (argument === "--package-manager") {
|
|
37
|
+
const value = args[++index];
|
|
38
|
+
if (!value) throw new Error("--package-manager requires pnpm, npm, yarn, or bun.");
|
|
39
|
+
if (value !== "pnpm" && value !== "npm" && value !== "yarn" && value !== "bun") throw new Error(`Unsupported package manager "${value}". Choose pnpm, npm, yarn, or bun.`);
|
|
40
|
+
packageManager = value;
|
|
41
|
+
} else if (argument === "--yes" || argument === "-y") yes = true;
|
|
42
|
+
else if (argument === "--template" || argument === "-t") {
|
|
43
|
+
const value = args[++index];
|
|
44
|
+
if (!value) throw new Error(`${argument} requires a template reference.`);
|
|
45
|
+
template = value;
|
|
46
|
+
} else if (argument === "--template-dir") {
|
|
47
|
+
const value = args[++index];
|
|
48
|
+
if (!value) throw new Error("--template-dir requires a directory.");
|
|
49
|
+
templateDirectory = value;
|
|
50
|
+
} else if (argument === "--var") {
|
|
51
|
+
const value = args[++index];
|
|
52
|
+
if (!value) throw new Error("--var requires key=value.");
|
|
53
|
+
const [key, parsed] = parseVariable(value);
|
|
54
|
+
if (Object.hasOwn(variables, key)) throw new Error(`Variable "${key}" was provided more than once.`);
|
|
55
|
+
variables[key] = parsed;
|
|
56
|
+
} else if (argument.startsWith("-")) {
|
|
57
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
58
|
+
} else positional.push(argument);
|
|
59
|
+
}
|
|
60
|
+
if (positional.length > 2) throw new Error('Too many arguments. Run "appinit create --help".');
|
|
61
|
+
if (positional.length === 2 && template) {
|
|
62
|
+
throw new Error("Do not combine --template with two positional arguments; use <template> <target>.");
|
|
63
|
+
}
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
...positional.length === 2 ? { template: positional[0], target: positional[1] } : {},
|
|
66
|
+
...positional.length === 1 ? { target: positional[0] } : {},
|
|
67
|
+
...template ? { template } : {},
|
|
68
|
+
dryRun,
|
|
69
|
+
yes,
|
|
70
|
+
...templateDirectory ? { templateDirectory } : {},
|
|
71
|
+
variables: Object.freeze(variables),
|
|
72
|
+
install,
|
|
73
|
+
git,
|
|
74
|
+
...packageManager ? { packageManager } : {}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/context.ts
|
|
79
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
80
|
+
import path8 from "path";
|
|
81
|
+
|
|
82
|
+
// ../os/src/runtime/fs/is-binary-file.ts
|
|
83
|
+
import path from "path";
|
|
84
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
85
|
+
".png",
|
|
86
|
+
".jpg",
|
|
87
|
+
".jpeg",
|
|
88
|
+
".gif",
|
|
89
|
+
".webp",
|
|
90
|
+
".ico",
|
|
91
|
+
".bmp",
|
|
92
|
+
".svg",
|
|
93
|
+
".svgz",
|
|
94
|
+
".woff",
|
|
95
|
+
".woff2",
|
|
96
|
+
".ttf",
|
|
97
|
+
".otf",
|
|
98
|
+
".eot",
|
|
99
|
+
".mp3",
|
|
100
|
+
".mp4",
|
|
101
|
+
".wav",
|
|
102
|
+
".ogg",
|
|
103
|
+
".webm",
|
|
104
|
+
".zip",
|
|
105
|
+
".tar",
|
|
106
|
+
".gz",
|
|
107
|
+
".tgz",
|
|
108
|
+
".rar",
|
|
109
|
+
".7z",
|
|
110
|
+
".pdf",
|
|
111
|
+
".exe",
|
|
112
|
+
".dll",
|
|
113
|
+
".bin"
|
|
114
|
+
]);
|
|
115
|
+
function isBinaryFile(filePath) {
|
|
116
|
+
return BINARY_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ../os/src/runtime/fs/path.ts
|
|
120
|
+
import path2 from "path";
|
|
121
|
+
|
|
122
|
+
// ../os/src/runtime/fs/traversal.ts
|
|
123
|
+
import { lstat, readdir } from "fs/promises";
|
|
124
|
+
import path3 from "path";
|
|
125
|
+
import picomatch from "picomatch";
|
|
126
|
+
async function readDirRecursive(root, options = {}) {
|
|
127
|
+
const {
|
|
128
|
+
prefix = "",
|
|
129
|
+
ignores = [],
|
|
130
|
+
mode = "relative"
|
|
131
|
+
} = options;
|
|
132
|
+
const absoluteRoot = path3.resolve(root);
|
|
133
|
+
const normalizedPrefix = prefix ? path3.posix.normalize(prefix.replace(/\\/g, "/")) : "";
|
|
134
|
+
if (normalizedPrefix === ".." || normalizedPrefix.startsWith("../") || path3.posix.isAbsolute(normalizedPrefix)) {
|
|
135
|
+
throw new Error("Traversal prefix must remain inside the root directory.");
|
|
136
|
+
}
|
|
137
|
+
const isIgnored = ignores.length > 0 ? picomatch(ignores, { dot: true }) : () => false;
|
|
138
|
+
const results = [];
|
|
139
|
+
async function walk2(directory, relativeDirectory) {
|
|
140
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
141
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
142
|
+
for (const entry of entries) {
|
|
143
|
+
const relativePath = path3.posix.join(relativeDirectory, entry.name);
|
|
144
|
+
if (isIgnored(relativePath)) continue;
|
|
145
|
+
const absolutePath = path3.join(directory, entry.name);
|
|
146
|
+
let details;
|
|
147
|
+
try {
|
|
148
|
+
details = await lstat(absolutePath);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error.code === "ENOENT") continue;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
if (details.isSymbolicLink()) continue;
|
|
154
|
+
if (details.isDirectory()) {
|
|
155
|
+
await walk2(absolutePath, relativePath);
|
|
156
|
+
} else if (details.isFile()) {
|
|
157
|
+
results.push(
|
|
158
|
+
mode === "absolute" ? absolutePath : relativePath
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
await walk2(absoluteRoot, normalizedPrefix);
|
|
164
|
+
return results.sort((a, b) => a.localeCompare(b));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ../os/src/runtime/vfs/freeze-vfs.ts
|
|
168
|
+
function freezeVFS(vfs) {
|
|
169
|
+
for (const file of vfs.list()) {
|
|
170
|
+
Object.freeze(file);
|
|
171
|
+
}
|
|
172
|
+
return vfs;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ../os/src/runtime/vfs/vfs.ts
|
|
176
|
+
import path4 from "path";
|
|
177
|
+
function normalizeVfsDestination(destination) {
|
|
178
|
+
if (typeof destination !== "string" || destination.trim().length === 0 || destination.includes("\0")) {
|
|
179
|
+
throw new Error("VFS destination must be a non-empty relative path.");
|
|
180
|
+
}
|
|
181
|
+
const portable = destination.replace(/\\/g, "/");
|
|
182
|
+
if (portable.startsWith("/") || portable.startsWith("//") || /^[a-zA-Z]:/.test(portable)) {
|
|
183
|
+
throw new Error(`VFS destination must be relative: ${destination}`);
|
|
184
|
+
}
|
|
185
|
+
let normalized = path4.posix.normalize(portable);
|
|
186
|
+
while (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
|
|
187
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../") || path4.posix.isAbsolute(normalized)) {
|
|
188
|
+
throw new Error(`VFS destination escapes the project root: ${destination}`);
|
|
189
|
+
}
|
|
190
|
+
return normalized;
|
|
191
|
+
}
|
|
192
|
+
var VirtualFileSystem = class {
|
|
193
|
+
files = /* @__PURE__ */ new Map();
|
|
194
|
+
add(file) {
|
|
195
|
+
const destination = normalizeVfsDestination(file.dest);
|
|
196
|
+
if (this.files.has(destination)) {
|
|
197
|
+
throw new Error(`VFS conflict: file already exists at ${destination}`);
|
|
198
|
+
}
|
|
199
|
+
this.files.set(destination, { ...file, dest: destination });
|
|
200
|
+
}
|
|
201
|
+
get(destination) {
|
|
202
|
+
return this.files.get(normalizeVfsDestination(destination));
|
|
203
|
+
}
|
|
204
|
+
list() {
|
|
205
|
+
return this.files.values();
|
|
206
|
+
}
|
|
207
|
+
remove(destination) {
|
|
208
|
+
return this.files.delete(normalizeVfsDestination(destination));
|
|
209
|
+
}
|
|
210
|
+
updateContent(destination, content) {
|
|
211
|
+
const file = this.get(destination);
|
|
212
|
+
if (!file) return;
|
|
213
|
+
if (Object.isFrozen(file)) {
|
|
214
|
+
throw new Error(`Attempted to mutate frozen VFS file: ${destination}`);
|
|
215
|
+
}
|
|
216
|
+
file.content = content;
|
|
217
|
+
}
|
|
218
|
+
get size() {
|
|
219
|
+
return this.files.size;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// ../os/src/engine/execute-file-operations.ts
|
|
224
|
+
import {
|
|
225
|
+
lstat as lstat2,
|
|
226
|
+
mkdir,
|
|
227
|
+
mkdtemp,
|
|
228
|
+
realpath,
|
|
229
|
+
rename,
|
|
230
|
+
rm,
|
|
231
|
+
rmdir,
|
|
232
|
+
writeFile
|
|
233
|
+
} from "fs/promises";
|
|
234
|
+
import path5 from "path";
|
|
235
|
+
function normalizeDestination(value) {
|
|
236
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
237
|
+
throw new Error("Destination must be a non-empty relative path.");
|
|
238
|
+
}
|
|
239
|
+
const portable = value.replace(/\\/g, "/");
|
|
240
|
+
if (portable.startsWith("/") || /^[a-zA-Z]:/.test(portable)) {
|
|
241
|
+
throw new Error("Absolute destinations are not allowed.");
|
|
242
|
+
}
|
|
243
|
+
if (portable.split("/").some((segment) => segment === ".." || segment === ".")) {
|
|
244
|
+
throw new Error("Dot and parent-directory segments are not allowed.");
|
|
245
|
+
}
|
|
246
|
+
return normalizeVfsDestination(portable);
|
|
247
|
+
}
|
|
248
|
+
async function exists(filePath) {
|
|
249
|
+
try {
|
|
250
|
+
await lstat2(filePath);
|
|
251
|
+
return true;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error.code === "ENOENT") return false;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function ensureSafeTargetDirectory(targetRoot, relativeDirectory) {
|
|
258
|
+
let current = targetRoot;
|
|
259
|
+
if (!relativeDirectory) return;
|
|
260
|
+
for (const segment of relativeDirectory.split(/[\\/]+/).filter(Boolean)) {
|
|
261
|
+
current = path5.join(current, segment);
|
|
262
|
+
if (await exists(current)) {
|
|
263
|
+
const info = await lstat2(current);
|
|
264
|
+
if (info.isSymbolicLink()) {
|
|
265
|
+
throw new Error(`Target path contains a symbolic link: ${current}`);
|
|
266
|
+
}
|
|
267
|
+
if (!info.isDirectory()) {
|
|
268
|
+
throw new Error(`Target path component is not a directory: ${current}`);
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
await mkdir(current);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function ensureSafeTargetRoot(targetDirectory) {
|
|
276
|
+
const absolute = path5.resolve(targetDirectory);
|
|
277
|
+
const parent = path5.dirname(absolute);
|
|
278
|
+
await mkdir(parent, { recursive: true });
|
|
279
|
+
const canonicalParent = await realpath(parent);
|
|
280
|
+
const root = path5.join(canonicalParent, path5.basename(absolute));
|
|
281
|
+
if (await exists(root)) {
|
|
282
|
+
const info = await lstat2(root);
|
|
283
|
+
if (info.isSymbolicLink()) throw new Error("Target directory must not be a symbolic link.");
|
|
284
|
+
if (!info.isDirectory()) throw new Error("Target path exists and is not a directory.");
|
|
285
|
+
} else {
|
|
286
|
+
await mkdir(root);
|
|
287
|
+
}
|
|
288
|
+
return await realpath(root);
|
|
289
|
+
}
|
|
290
|
+
async function executeFileOperations(targetDirectory, operations, options = {}) {
|
|
291
|
+
const diagnostics = [];
|
|
292
|
+
const normalized = [];
|
|
293
|
+
const seen = /* @__PURE__ */ new Set();
|
|
294
|
+
for (const operation of operations) {
|
|
295
|
+
try {
|
|
296
|
+
const destination = normalizeDestination(operation.destination);
|
|
297
|
+
if (seen.has(destination)) {
|
|
298
|
+
diagnostics.push({
|
|
299
|
+
code: "EXECUTION_DUPLICATE_PATH",
|
|
300
|
+
message: `Execution plan contains duplicate destination "${destination}".`,
|
|
301
|
+
destination
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
seen.add(destination);
|
|
305
|
+
normalized.push({ ...operation, destination });
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
diagnostics.push({
|
|
309
|
+
code: "EXECUTION_INVALID_PATH",
|
|
310
|
+
message: error instanceof Error ? error.message : String(error),
|
|
311
|
+
destination: operation.destination
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (diagnostics.length > 0) {
|
|
316
|
+
return Object.freeze({
|
|
317
|
+
ok: false,
|
|
318
|
+
dryRun: options.dryRun === true,
|
|
319
|
+
writtenFiles: Object.freeze([]),
|
|
320
|
+
overwrittenFiles: Object.freeze([]),
|
|
321
|
+
diagnostics: Object.freeze(diagnostics)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
if (typeof targetDirectory !== "string" || targetDirectory.trim().length === 0) {
|
|
325
|
+
return Object.freeze({
|
|
326
|
+
ok: false,
|
|
327
|
+
dryRun: options.dryRun === true,
|
|
328
|
+
writtenFiles: Object.freeze([]),
|
|
329
|
+
overwrittenFiles: Object.freeze([]),
|
|
330
|
+
diagnostics: Object.freeze([{
|
|
331
|
+
code: "EXECUTION_INVALID_TARGET",
|
|
332
|
+
message: "Target directory must be a non-empty path."
|
|
333
|
+
}])
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (options.dryRun) {
|
|
337
|
+
return Object.freeze({
|
|
338
|
+
ok: true,
|
|
339
|
+
dryRun: true,
|
|
340
|
+
writtenFiles: Object.freeze(normalized.map((item) => item.destination)),
|
|
341
|
+
overwrittenFiles: Object.freeze([]),
|
|
342
|
+
diagnostics: Object.freeze([])
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const targetExistedBeforeExecution = await exists(path5.resolve(targetDirectory));
|
|
346
|
+
let targetRoot;
|
|
347
|
+
try {
|
|
348
|
+
targetRoot = await ensureSafeTargetRoot(targetDirectory);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return Object.freeze({
|
|
351
|
+
ok: false,
|
|
352
|
+
dryRun: false,
|
|
353
|
+
writtenFiles: Object.freeze([]),
|
|
354
|
+
overwrittenFiles: Object.freeze([]),
|
|
355
|
+
diagnostics: Object.freeze([{
|
|
356
|
+
code: "EXECUTION_INVALID_TARGET",
|
|
357
|
+
message: error instanceof Error ? error.message : String(error)
|
|
358
|
+
}])
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const parent = path5.dirname(targetRoot);
|
|
362
|
+
const stagingRoot = await mkdtemp(path5.join(parent, ".appinit-stage-"));
|
|
363
|
+
const payloadRoot = path5.join(stagingRoot, "payload");
|
|
364
|
+
const backupRoot = path5.join(stagingRoot, "backup");
|
|
365
|
+
await mkdir(payloadRoot);
|
|
366
|
+
await mkdir(backupRoot);
|
|
367
|
+
const writtenFiles = [];
|
|
368
|
+
const overwrittenFiles = [];
|
|
369
|
+
const installed = [];
|
|
370
|
+
const backups = [];
|
|
371
|
+
const createdDirectories = [];
|
|
372
|
+
try {
|
|
373
|
+
for (const operation of normalized) {
|
|
374
|
+
const stagedPath = path5.join(payloadRoot, ...operation.destination.split("/"));
|
|
375
|
+
await mkdir(path5.dirname(stagedPath), { recursive: true });
|
|
376
|
+
await writeFile(stagedPath, operation.content);
|
|
377
|
+
}
|
|
378
|
+
for (const operation of normalized) {
|
|
379
|
+
const destinationPath = path5.join(targetRoot, ...operation.destination.split("/"));
|
|
380
|
+
const relativeParent = path5.dirname(operation.destination);
|
|
381
|
+
const before = /* @__PURE__ */ new Set();
|
|
382
|
+
let cursor = targetRoot;
|
|
383
|
+
for (const segment of relativeParent === "." ? [] : relativeParent.split("/")) {
|
|
384
|
+
cursor = path5.join(cursor, segment);
|
|
385
|
+
if (!await exists(cursor)) before.add(cursor);
|
|
386
|
+
}
|
|
387
|
+
await ensureSafeTargetDirectory(targetRoot, relativeParent);
|
|
388
|
+
for (const directory of before) createdDirectories.push(directory);
|
|
389
|
+
if (await exists(destinationPath)) {
|
|
390
|
+
const info = await lstat2(destinationPath);
|
|
391
|
+
if (info.isSymbolicLink()) {
|
|
392
|
+
throw new Error(`Refusing to overwrite symbolic link: ${operation.destination}`);
|
|
393
|
+
}
|
|
394
|
+
if (!info.isFile()) {
|
|
395
|
+
throw new Error(`Destination exists and is not a regular file: ${operation.destination}`);
|
|
396
|
+
}
|
|
397
|
+
const backupPath = path5.join(backupRoot, ...operation.destination.split("/"));
|
|
398
|
+
await mkdir(path5.dirname(backupPath), { recursive: true });
|
|
399
|
+
await rename(destinationPath, backupPath);
|
|
400
|
+
backups.push({ backup: backupPath, destination: destinationPath });
|
|
401
|
+
overwrittenFiles.push(operation.destination);
|
|
402
|
+
} else {
|
|
403
|
+
writtenFiles.push(operation.destination);
|
|
404
|
+
}
|
|
405
|
+
const stagedPath = path5.join(payloadRoot, ...operation.destination.split("/"));
|
|
406
|
+
await rename(stagedPath, destinationPath);
|
|
407
|
+
installed.push(destinationPath);
|
|
408
|
+
}
|
|
409
|
+
await rm(stagingRoot, { recursive: true, force: true });
|
|
410
|
+
return Object.freeze({
|
|
411
|
+
ok: true,
|
|
412
|
+
dryRun: false,
|
|
413
|
+
writtenFiles: Object.freeze(writtenFiles),
|
|
414
|
+
overwrittenFiles: Object.freeze(overwrittenFiles),
|
|
415
|
+
diagnostics: Object.freeze([])
|
|
416
|
+
});
|
|
417
|
+
} catch (error) {
|
|
418
|
+
const rollbackErrors = [];
|
|
419
|
+
for (const destination of installed.reverse()) {
|
|
420
|
+
await rm(destination, { force: true }).catch((rollbackError) => {
|
|
421
|
+
rollbackErrors.push(`Could not remove "${destination}": ${String(rollbackError)}`);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
for (const item of backups.reverse()) {
|
|
425
|
+
await mkdir(path5.dirname(item.destination), { recursive: true }).catch((rollbackError) => {
|
|
426
|
+
rollbackErrors.push(`Could not recreate parent for "${item.destination}": ${String(rollbackError)}`);
|
|
427
|
+
});
|
|
428
|
+
await rename(item.backup, item.destination).catch((rollbackError) => {
|
|
429
|
+
rollbackErrors.push(`Could not restore "${item.destination}": ${String(rollbackError)}`);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
for (const directory of createdDirectories.reverse()) {
|
|
433
|
+
await rmdir(directory).catch(() => void 0);
|
|
434
|
+
}
|
|
435
|
+
if (!targetExistedBeforeExecution) {
|
|
436
|
+
await rmdir(targetRoot).catch(() => void 0);
|
|
437
|
+
}
|
|
438
|
+
await rm(stagingRoot, { recursive: true, force: true }).catch((rollbackError) => {
|
|
439
|
+
rollbackErrors.push(`Could not remove staging directory: ${String(rollbackError)}`);
|
|
440
|
+
});
|
|
441
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
442
|
+
return Object.freeze({
|
|
443
|
+
ok: false,
|
|
444
|
+
dryRun: false,
|
|
445
|
+
writtenFiles: Object.freeze([]),
|
|
446
|
+
overwrittenFiles: Object.freeze([]),
|
|
447
|
+
diagnostics: Object.freeze([{
|
|
448
|
+
code: "EXECUTION_COMMIT_FAILED",
|
|
449
|
+
message: rollbackErrors.length > 0 ? `${cause}. Rollback also reported: ${rollbackErrors.join("; ")}` : cause
|
|
450
|
+
}])
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ../kernel/src/execution/phase.ts
|
|
456
|
+
var LIFECYCLE_ORDER = [
|
|
457
|
+
"detect",
|
|
458
|
+
"validate",
|
|
459
|
+
"resolve",
|
|
460
|
+
"plan",
|
|
461
|
+
"execute",
|
|
462
|
+
"verify",
|
|
463
|
+
"report"
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
// ../kernel/src/template/manifest.ts
|
|
467
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
468
|
+
"schemaVersion",
|
|
469
|
+
"id",
|
|
470
|
+
"version",
|
|
471
|
+
"kind",
|
|
472
|
+
"atsVersion",
|
|
473
|
+
"name",
|
|
474
|
+
"description",
|
|
475
|
+
"category",
|
|
476
|
+
"language",
|
|
477
|
+
"packageName",
|
|
478
|
+
"renderer",
|
|
479
|
+
"paths",
|
|
480
|
+
"tags",
|
|
481
|
+
"requires",
|
|
482
|
+
"provides",
|
|
483
|
+
"conflicts"
|
|
484
|
+
]);
|
|
485
|
+
var TEMPLATE_KINDS = /* @__PURE__ */ new Set([
|
|
486
|
+
"project",
|
|
487
|
+
"module",
|
|
488
|
+
"capability",
|
|
489
|
+
"preset",
|
|
490
|
+
"composition"
|
|
491
|
+
]);
|
|
492
|
+
var SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
493
|
+
var ID_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
494
|
+
var ATS_PATTERN = /^\d+\.\d+$/;
|
|
495
|
+
var PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
496
|
+
function isRecord(value) {
|
|
497
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
498
|
+
}
|
|
499
|
+
function isNonEmptyString(value) {
|
|
500
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
501
|
+
}
|
|
502
|
+
function isSafeRelativePath(value) {
|
|
503
|
+
if (!isNonEmptyString(value) || value.includes("\\") || value.startsWith("/")) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
const segments = value.replace(/^\.\//, "").split("/");
|
|
507
|
+
return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
508
|
+
}
|
|
509
|
+
function validateTemplateManifest(value) {
|
|
510
|
+
const errors = [];
|
|
511
|
+
if (!isRecord(value)) {
|
|
512
|
+
return { valid: false, errors: ["Manifest must be a JSON object."] };
|
|
513
|
+
}
|
|
514
|
+
for (const key of Object.keys(value)) {
|
|
515
|
+
if (!ALLOWED_KEYS.has(key)) errors.push(`Unknown manifest field: ${key}`);
|
|
516
|
+
}
|
|
517
|
+
if (value.schemaVersion !== 1) {
|
|
518
|
+
errors.push("schemaVersion must be 1.");
|
|
519
|
+
}
|
|
520
|
+
if (!isNonEmptyString(value.id) || !ID_PATTERN.test(value.id)) {
|
|
521
|
+
errors.push("id must use lowercase dot- or hyphen-separated identifiers.");
|
|
522
|
+
}
|
|
523
|
+
if (!isNonEmptyString(value.version) || !SEMVER_PATTERN.test(value.version)) {
|
|
524
|
+
errors.push("version must be a valid semantic version.");
|
|
525
|
+
}
|
|
526
|
+
if (!isNonEmptyString(value.kind) || !TEMPLATE_KINDS.has(value.kind)) {
|
|
527
|
+
errors.push("kind must be project, module, capability, preset, or composition.");
|
|
528
|
+
}
|
|
529
|
+
if (!isNonEmptyString(value.atsVersion) || !ATS_PATTERN.test(value.atsVersion)) {
|
|
530
|
+
errors.push("atsVersion must use the MAJOR.MINOR format (for example, 2.0).");
|
|
531
|
+
}
|
|
532
|
+
if (!isNonEmptyString(value.name)) errors.push("name is required.");
|
|
533
|
+
if (!isNonEmptyString(value.description)) errors.push("description is required.");
|
|
534
|
+
if (!isNonEmptyString(value.category)) errors.push("category is required.");
|
|
535
|
+
if (!isNonEmptyString(value.language)) errors.push("language is required.");
|
|
536
|
+
if (!isNonEmptyString(value.packageName) || !PACKAGE_NAME_PATTERN.test(value.packageName)) {
|
|
537
|
+
errors.push("packageName must be a valid npm package name.");
|
|
538
|
+
}
|
|
539
|
+
if (!isRecord(value.renderer)) {
|
|
540
|
+
errors.push("renderer must be an object.");
|
|
541
|
+
} else if (value.renderer.engine !== "liquid" && value.renderer.engine !== "none") {
|
|
542
|
+
errors.push('renderer.engine must be "liquid" or "none".');
|
|
543
|
+
}
|
|
544
|
+
if (!isRecord(value.paths)) {
|
|
545
|
+
errors.push("paths must be an object.");
|
|
546
|
+
} else {
|
|
547
|
+
if (!isSafeRelativePath(value.paths.template)) {
|
|
548
|
+
errors.push("paths.template must be a safe relative path.");
|
|
549
|
+
}
|
|
550
|
+
for (const key of ["variables", "hooks"]) {
|
|
551
|
+
if (value.paths[key] !== void 0 && !isSafeRelativePath(value.paths[key])) {
|
|
552
|
+
errors.push(`paths.${key} must be a safe relative path when provided.`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (!Array.isArray(value.tags) || !value.tags.every((tag) => isNonEmptyString(tag))) {
|
|
557
|
+
errors.push("tags must be an array of non-empty strings.");
|
|
558
|
+
}
|
|
559
|
+
for (const key of ["requires", "provides", "conflicts"]) {
|
|
560
|
+
const entries = value[key];
|
|
561
|
+
if (entries === void 0) continue;
|
|
562
|
+
if (!Array.isArray(entries) || !entries.every((entry) => isNonEmptyString(entry))) {
|
|
563
|
+
errors.push(`${key} must be an array of non-empty strings when provided.`);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (new Set(entries).size !== entries.length) {
|
|
567
|
+
errors.push(`${key} must not contain duplicate entries.`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (errors.length > 0) return { valid: false, errors };
|
|
571
|
+
return {
|
|
572
|
+
valid: true,
|
|
573
|
+
errors: [],
|
|
574
|
+
manifest: value
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ../kernel/src/template/registry.ts
|
|
579
|
+
var CATALOG_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "updatedAt", "templates"]);
|
|
580
|
+
var ENTRY_KEYS = /* @__PURE__ */ new Set([
|
|
581
|
+
"id",
|
|
582
|
+
"version",
|
|
583
|
+
"name",
|
|
584
|
+
"description",
|
|
585
|
+
"category",
|
|
586
|
+
"language",
|
|
587
|
+
"tags",
|
|
588
|
+
"archive"
|
|
589
|
+
]);
|
|
590
|
+
var ARCHIVE_KEYS = /* @__PURE__ */ new Set(["url", "sha256"]);
|
|
591
|
+
var ID_PATTERN2 = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
592
|
+
var SEMVER_PATTERN2 = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
593
|
+
var SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
594
|
+
var MAX_CATALOG_ENTRIES = 1e4;
|
|
595
|
+
var MAX_TEXT_LENGTH = 2e3;
|
|
596
|
+
function isRecord2(value) {
|
|
597
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
598
|
+
}
|
|
599
|
+
function isText(value) {
|
|
600
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_TEXT_LENGTH;
|
|
601
|
+
}
|
|
602
|
+
function hasOnlyKeys(value, allowed, prefix, errors) {
|
|
603
|
+
for (const key of Object.keys(value)) {
|
|
604
|
+
if (!allowed.has(key)) errors.push(`Unknown ${prefix} field: ${key}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function isHttpsUrl(value) {
|
|
608
|
+
if (typeof value !== "string" || value.length > 4096) return false;
|
|
609
|
+
try {
|
|
610
|
+
const url = new URL(value);
|
|
611
|
+
return url.protocol === "https:" && url.hostname.length > 0 && url.username.length === 0 && url.password.length === 0 && url.hash.length === 0;
|
|
612
|
+
} catch {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function isIsoDateTime(value) {
|
|
617
|
+
if (typeof value !== "string" || value.length > 64 || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value)) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
const timestamp = Date.parse(value);
|
|
621
|
+
if (!Number.isFinite(timestamp)) return false;
|
|
622
|
+
const fraction = /\.(\d{1,3})Z$/.exec(value)?.[1] ?? "";
|
|
623
|
+
const normalized = value.replace(
|
|
624
|
+
/(?:\.\d{1,3})?Z$/,
|
|
625
|
+
"." + fraction.padEnd(3, "0") + "Z"
|
|
626
|
+
);
|
|
627
|
+
return new Date(timestamp).toISOString() === normalized;
|
|
628
|
+
}
|
|
629
|
+
function validateTemplateRegistryCatalog(value) {
|
|
630
|
+
const errors = [];
|
|
631
|
+
if (!isRecord2(value)) {
|
|
632
|
+
return { valid: false, errors: ["Registry catalog must be a JSON object."] };
|
|
633
|
+
}
|
|
634
|
+
hasOnlyKeys(value, CATALOG_KEYS, "catalog", errors);
|
|
635
|
+
if (value.schemaVersion !== 1) {
|
|
636
|
+
errors.push("schemaVersion must be 1.");
|
|
637
|
+
}
|
|
638
|
+
if (value.updatedAt !== void 0 && !isIsoDateTime(value.updatedAt)) {
|
|
639
|
+
errors.push("updatedAt must be an ISO-8601 UTC timestamp.");
|
|
640
|
+
}
|
|
641
|
+
if (!Array.isArray(value.templates)) {
|
|
642
|
+
errors.push("templates must be an array.");
|
|
643
|
+
return { valid: false, errors };
|
|
644
|
+
}
|
|
645
|
+
if (value.templates.length > MAX_CATALOG_ENTRIES) {
|
|
646
|
+
errors.push(`templates must contain no more than ${MAX_CATALOG_ENTRIES} entries.`);
|
|
647
|
+
return { valid: false, errors };
|
|
648
|
+
}
|
|
649
|
+
const identities = /* @__PURE__ */ new Set();
|
|
650
|
+
value.templates.forEach((entry, index) => {
|
|
651
|
+
const prefix = `templates[${index}]`;
|
|
652
|
+
if (!isRecord2(entry)) {
|
|
653
|
+
errors.push(`${prefix} must be an object.`);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
hasOnlyKeys(entry, ENTRY_KEYS, prefix, errors);
|
|
657
|
+
if (!isText(entry.id) || !ID_PATTERN2.test(entry.id)) {
|
|
658
|
+
errors.push(`${prefix}.id must use lowercase dot- or hyphen-separated identifiers.`);
|
|
659
|
+
}
|
|
660
|
+
if (!isText(entry.version) || !SEMVER_PATTERN2.test(entry.version)) {
|
|
661
|
+
errors.push(`${prefix}.version must be a valid semantic version.`);
|
|
662
|
+
}
|
|
663
|
+
for (const key of ["name", "description", "category", "language"]) {
|
|
664
|
+
if (!isText(entry[key])) errors.push(`${prefix}.${key} must be a non-empty string.`);
|
|
665
|
+
}
|
|
666
|
+
if (!Array.isArray(entry.tags) || !entry.tags.every((tag) => isText(tag))) {
|
|
667
|
+
errors.push(`${prefix}.tags must be an array of non-empty strings.`);
|
|
668
|
+
} else if (new Set(entry.tags).size !== entry.tags.length) {
|
|
669
|
+
errors.push(`${prefix}.tags must not contain duplicate entries.`);
|
|
670
|
+
}
|
|
671
|
+
if (!isRecord2(entry.archive)) {
|
|
672
|
+
errors.push(`${prefix}.archive must be an object.`);
|
|
673
|
+
} else {
|
|
674
|
+
hasOnlyKeys(entry.archive, ARCHIVE_KEYS, `${prefix}.archive`, errors);
|
|
675
|
+
if (!isHttpsUrl(entry.archive.url)) {
|
|
676
|
+
errors.push(`${prefix}.archive.url must be an HTTPS URL without credentials or a fragment.`);
|
|
677
|
+
}
|
|
678
|
+
if (typeof entry.archive.sha256 !== "string" || !SHA256_PATTERN.test(entry.archive.sha256)) {
|
|
679
|
+
errors.push(`${prefix}.archive.sha256 must be a lowercase 64-character SHA-256 digest.`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (typeof entry.id === "string" && typeof entry.version === "string" && ID_PATTERN2.test(entry.id) && SEMVER_PATTERN2.test(entry.version)) {
|
|
683
|
+
const identity = `${entry.id}@${entry.version}`;
|
|
684
|
+
if (identities.has(identity)) {
|
|
685
|
+
errors.push(`${prefix} duplicates template ${identity}.`);
|
|
686
|
+
}
|
|
687
|
+
identities.add(identity);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
if (errors.length > 0) return { valid: false, errors };
|
|
691
|
+
return {
|
|
692
|
+
valid: true,
|
|
693
|
+
errors: [],
|
|
694
|
+
catalog: value
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ../kernel/src/plan/execution-plan-validator.ts
|
|
699
|
+
var phaseOrder = new Map(LIFECYCLE_ORDER.map((phase, index) => [phase, index]));
|
|
700
|
+
|
|
701
|
+
// ../os/src/engine/validation/validate-execution-intent.ts
|
|
702
|
+
var EXECUTION_KINDS = /* @__PURE__ */ new Set(["scaffold", "generate", "preview", "validate"]);
|
|
703
|
+
var INVOCATION_MODES = /* @__PURE__ */ new Set(["user", "author", "ci", "system"]);
|
|
704
|
+
var ARTIFACT_KINDS = /* @__PURE__ */ new Set([
|
|
705
|
+
"preset",
|
|
706
|
+
"module",
|
|
707
|
+
"project",
|
|
708
|
+
"template",
|
|
709
|
+
"capability",
|
|
710
|
+
"composition"
|
|
711
|
+
]);
|
|
712
|
+
function isRecord3(value) {
|
|
713
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
714
|
+
const prototype = Object.getPrototypeOf(value);
|
|
715
|
+
return prototype === Object.prototype || prototype === null;
|
|
716
|
+
}
|
|
717
|
+
function isJsonValue(value, seen = /* @__PURE__ */ new Set()) {
|
|
718
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
719
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
720
|
+
if (typeof value !== "object") return false;
|
|
721
|
+
if (seen.has(value)) return false;
|
|
722
|
+
seen.add(value);
|
|
723
|
+
if (Array.isArray(value)) {
|
|
724
|
+
const valid2 = value.every((entry) => isJsonValue(entry, seen));
|
|
725
|
+
seen.delete(value);
|
|
726
|
+
return valid2;
|
|
727
|
+
}
|
|
728
|
+
if (!isRecord3(value)) return false;
|
|
729
|
+
const valid = Object.values(value).every((entry) => isJsonValue(entry, seen));
|
|
730
|
+
seen.delete(value);
|
|
731
|
+
return valid;
|
|
732
|
+
}
|
|
733
|
+
function validateExecutionIntent(input) {
|
|
734
|
+
const diagnostics = [];
|
|
735
|
+
const add = (code, message, field) => {
|
|
736
|
+
diagnostics.push({ code, message, ...field ? { field } : {} });
|
|
737
|
+
};
|
|
738
|
+
if (!isRecord3(input)) {
|
|
739
|
+
add("INTENT_INVALID_SHAPE", "Execution intent must be a plain object.");
|
|
740
|
+
return Object.freeze({ ok: false, diagnostics: Object.freeze(diagnostics) });
|
|
741
|
+
}
|
|
742
|
+
if (typeof input.executionKind !== "string" || !EXECUTION_KINDS.has(input.executionKind)) {
|
|
743
|
+
add("INTENT_INVALID_EXECUTION_KIND", "executionKind must be scaffold, generate, preview, or validate.", "executionKind");
|
|
744
|
+
}
|
|
745
|
+
if (typeof input.executionMode !== "string" || !INVOCATION_MODES.has(input.executionMode)) {
|
|
746
|
+
add("INTENT_INVALID_INVOCATION_MODE", "executionMode must be user, author, ci, or system.", "executionMode");
|
|
747
|
+
}
|
|
748
|
+
if (typeof input.artifactKind !== "string" || !ARTIFACT_KINDS.has(input.artifactKind)) {
|
|
749
|
+
add("INTENT_INVALID_ARTIFACT_KIND", "artifactKind must be a supported AppInit artifact kind.", "artifactKind");
|
|
750
|
+
}
|
|
751
|
+
if (typeof input.rootDir !== "string" || input.rootDir.trim().length === 0) {
|
|
752
|
+
add("INTENT_ROOT_REQUIRED", "rootDir must be a non-empty string.", "rootDir");
|
|
753
|
+
}
|
|
754
|
+
const kind = input.executionKind;
|
|
755
|
+
if ((kind === "scaffold" || kind === "generate") && (typeof input.targetDir !== "string" || input.targetDir.trim().length === 0)) {
|
|
756
|
+
add("INTENT_TARGET_REQUIRED", `targetDir is required for executionKind "${kind}".`, "targetDir");
|
|
757
|
+
}
|
|
758
|
+
if ((kind === "scaffold" || kind === "generate" || kind === "preview") && (typeof input.templateRef !== "string" || input.templateRef.trim().length === 0)) {
|
|
759
|
+
add("INTENT_TEMPLATE_REQUIRED", `templateRef is required for executionKind "${kind}".`, "templateRef");
|
|
760
|
+
}
|
|
761
|
+
if (input.variables !== void 0 && (!isRecord3(input.variables) || !isJsonValue(input.variables))) {
|
|
762
|
+
add("INTENT_VARIABLES_INVALID", "variables must be a plain JSON-serializable object.", "variables");
|
|
763
|
+
}
|
|
764
|
+
if (input.flags !== void 0) {
|
|
765
|
+
if (!isRecord3(input.flags)) {
|
|
766
|
+
add("INTENT_FLAGS_INVALID", "flags must be a plain object.", "flags");
|
|
767
|
+
} else {
|
|
768
|
+
for (const key of ["dryRun", "debug", "skipInstall", "watch", "author"]) {
|
|
769
|
+
if (input.flags[key] !== void 0 && typeof input.flags[key] !== "boolean") {
|
|
770
|
+
add("INTENT_FLAGS_INVALID", `flags.${key} must be a boolean when provided.`, `flags.${key}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const unknownFlags = Object.keys(input.flags).filter(
|
|
774
|
+
(key) => !["dryRun", "debug", "skipInstall", "watch", "author"].includes(key)
|
|
775
|
+
);
|
|
776
|
+
for (const key of unknownFlags) {
|
|
777
|
+
add("INTENT_FLAGS_INVALID", `Unknown execution flag "${key}".`, `flags.${key}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (diagnostics.length > 0) {
|
|
782
|
+
return Object.freeze({ ok: false, diagnostics: Object.freeze(diagnostics) });
|
|
783
|
+
}
|
|
784
|
+
return Object.freeze({
|
|
785
|
+
ok: true,
|
|
786
|
+
diagnostics: Object.freeze([]),
|
|
787
|
+
intent: input
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ../os/src/engine/render/filters.ts
|
|
792
|
+
function registerAppInitFilters(engine2) {
|
|
793
|
+
engine2.registerFilter(
|
|
794
|
+
"kebab",
|
|
795
|
+
(value) => String(value).trim().replace(/\s+/g, "-").toLowerCase()
|
|
796
|
+
);
|
|
797
|
+
engine2.registerFilter(
|
|
798
|
+
"camel",
|
|
799
|
+
(value) => String(value).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase())
|
|
800
|
+
);
|
|
801
|
+
engine2.registerFilter("json", (value) => JSON.stringify(value, null, 2));
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ../os/src/engine/render/render-liquid.ts
|
|
805
|
+
import { Liquid } from "liquidjs";
|
|
806
|
+
var engine = new Liquid({
|
|
807
|
+
strictVariables: true,
|
|
808
|
+
strictFilters: true,
|
|
809
|
+
ownPropertyOnly: true,
|
|
810
|
+
lenientIf: false
|
|
811
|
+
});
|
|
812
|
+
registerAppInitFilters(engine);
|
|
813
|
+
async function renderLiquid(template, context) {
|
|
814
|
+
return engine.parseAndRender(template, context);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ../os/src/engine/render/render-with-engine.ts
|
|
818
|
+
function getPath(context, key) {
|
|
819
|
+
return key.split(".").reduce((value, segment) => {
|
|
820
|
+
if (value === null || typeof value !== "object" || !Object.prototype.hasOwnProperty.call(value, segment)) {
|
|
821
|
+
return void 0;
|
|
822
|
+
}
|
|
823
|
+
return value[segment];
|
|
824
|
+
}, context);
|
|
825
|
+
}
|
|
826
|
+
function assertNoLiquidSyntax(content, filePath, engine2) {
|
|
827
|
+
if (/\{\{|\{%|\{\#/s.test(content)) {
|
|
828
|
+
throw new Error(`Liquid syntax is not allowed in ${filePath} when renderer is "${engine2}".`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function replaceDatPlaceholders(content, variables) {
|
|
832
|
+
return content.replace(/%%\s*([\w.-]+)\s*%%/g, (_match, key) => {
|
|
833
|
+
const value = getPath(variables, key);
|
|
834
|
+
return value == null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
async function renderWithEngine(engine2, template, variables, filePath = "<virtual>") {
|
|
838
|
+
if (engine2 === "liquid") return replaceDatPlaceholders(await renderLiquid(template, variables), variables);
|
|
839
|
+
if (engine2 === "none") {
|
|
840
|
+
assertNoLiquidSyntax(template, filePath, engine2);
|
|
841
|
+
return replaceDatPlaceholders(template, variables);
|
|
842
|
+
}
|
|
843
|
+
throw new Error(`Unsupported template engine: ${engine2}`);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// ../os/src/engine/render/render-file.ts
|
|
847
|
+
async function renderFile(options) {
|
|
848
|
+
const { file, engine: engine2, variables } = options;
|
|
849
|
+
if (file.binary) {
|
|
850
|
+
if (!Buffer.isBuffer(file.content)) throw new Error(`Binary template file "${file.dest}" must contain Buffer data.`);
|
|
851
|
+
return { dest: file.dest, content: Buffer.from(file.content) };
|
|
852
|
+
}
|
|
853
|
+
if (typeof file.content !== "string") {
|
|
854
|
+
throw new Error(`Text template file "${file.dest}" must contain string data.`);
|
|
855
|
+
}
|
|
856
|
+
if (!file.render) return { dest: file.dest, content: file.content };
|
|
857
|
+
return {
|
|
858
|
+
dest: file.dest,
|
|
859
|
+
content: await renderWithEngine(engine2, file.content, variables, file.dest)
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// ../os/src/engine/render/validate-variables.ts
|
|
864
|
+
function validateTemplateVariables(variables, schema = {}) {
|
|
865
|
+
const errors = [];
|
|
866
|
+
if (variables === null || typeof variables !== "object" || Array.isArray(variables) || Object.getPrototypeOf(variables) !== Object.prototype && Object.getPrototypeOf(variables) !== null) {
|
|
867
|
+
return { valid: false, errors: ["Variables must be a plain object."] };
|
|
868
|
+
}
|
|
869
|
+
const values = variables;
|
|
870
|
+
for (const [key, rule] of Object.entries(schema)) {
|
|
871
|
+
const value = values[key];
|
|
872
|
+
if (value === void 0 || value === null) {
|
|
873
|
+
if (rule.required) errors.push(`"${key}" is required.`);
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
const validType = rule.type === "array" && Array.isArray(value) || rule.type === "object" && value !== null && typeof value === "object" && !Array.isArray(value) || rule.type !== "array" && rule.type !== "object" && typeof value === rule.type;
|
|
877
|
+
if (!validType) {
|
|
878
|
+
errors.push(`"${key}" must be a ${rule.type}.`);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
if (typeof value === "string") {
|
|
882
|
+
if (rule.minLength !== void 0 && value.length < rule.minLength) errors.push(`"${key}" must contain at least ${rule.minLength} characters.`);
|
|
883
|
+
if (rule.maxLength !== void 0 && value.length > rule.maxLength) errors.push(`"${key}" must contain at most ${rule.maxLength} characters.`);
|
|
884
|
+
}
|
|
885
|
+
if (rule.enum && !rule.enum.some((allowed) => Object.is(allowed, value))) {
|
|
886
|
+
errors.push(`"${key}" must be one of: ${rule.enum.join(", ")}.`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return Object.freeze({ valid: errors.length === 0, errors: Object.freeze(errors) });
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ../os/src/engine/verify-generated-output.ts
|
|
893
|
+
import { lstat as lstat3, readFile, readdir as readdir2, realpath as realpath2 } from "fs/promises";
|
|
894
|
+
import path6 from "path";
|
|
895
|
+
function isWithin(parent, child) {
|
|
896
|
+
const relative = path6.relative(parent, child);
|
|
897
|
+
return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
|
|
898
|
+
}
|
|
899
|
+
function sameContent(actual, expected) {
|
|
900
|
+
const expectedBytes = Buffer.isBuffer(expected) ? expected : Buffer.from(expected, "utf8");
|
|
901
|
+
return actual.equals(expectedBytes);
|
|
902
|
+
}
|
|
903
|
+
async function verifyGeneratedOutput(targetDirectory, operations, options = {}) {
|
|
904
|
+
const diagnostics = [];
|
|
905
|
+
const expected = /* @__PURE__ */ new Map();
|
|
906
|
+
for (const operation of operations) {
|
|
907
|
+
try {
|
|
908
|
+
const destination = normalizeVfsDestination(operation.destination);
|
|
909
|
+
if (expected.has(destination)) {
|
|
910
|
+
diagnostics.push({
|
|
911
|
+
code: "VERIFY_EXPECTED_PATH_DUPLICATE",
|
|
912
|
+
destination,
|
|
913
|
+
message: `Execution plan contains duplicate expected path "${destination}".`
|
|
914
|
+
});
|
|
915
|
+
} else {
|
|
916
|
+
expected.set(destination, operation);
|
|
917
|
+
}
|
|
918
|
+
} catch (error) {
|
|
919
|
+
diagnostics.push({
|
|
920
|
+
code: "VERIFY_EXPECTED_PATH_INVALID",
|
|
921
|
+
destination: operation.destination,
|
|
922
|
+
message: error instanceof Error ? error.message : String(error)
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
let root;
|
|
927
|
+
try {
|
|
928
|
+
const info = await lstat3(targetDirectory);
|
|
929
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
930
|
+
throw new Error("Target directory must be a real directory, not a symlink or file.");
|
|
931
|
+
}
|
|
932
|
+
root = await realpath2(targetDirectory);
|
|
933
|
+
} catch (error) {
|
|
934
|
+
diagnostics.push({
|
|
935
|
+
code: "VERIFY_TARGET_MISSING",
|
|
936
|
+
message: error instanceof Error ? error.message : String(error)
|
|
937
|
+
});
|
|
938
|
+
return Object.freeze({
|
|
939
|
+
ok: false,
|
|
940
|
+
targetDirectory,
|
|
941
|
+
expectedFiles: Object.freeze([...expected.keys()].sort()),
|
|
942
|
+
actualFiles: Object.freeze([]),
|
|
943
|
+
diagnostics: Object.freeze(diagnostics)
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
const actual = /* @__PURE__ */ new Set();
|
|
947
|
+
async function walk2(directory, relativeDirectory) {
|
|
948
|
+
let entries;
|
|
949
|
+
try {
|
|
950
|
+
entries = await readdir2(directory, { withFileTypes: true });
|
|
951
|
+
} catch (error) {
|
|
952
|
+
diagnostics.push({
|
|
953
|
+
code: "VERIFY_READ_FAILED",
|
|
954
|
+
destination: relativeDirectory || void 0,
|
|
955
|
+
message: error instanceof Error ? error.message : String(error)
|
|
956
|
+
});
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
960
|
+
for (const entry of entries) {
|
|
961
|
+
const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
|
|
962
|
+
const absolute = path6.join(directory, entry.name);
|
|
963
|
+
let info;
|
|
964
|
+
try {
|
|
965
|
+
info = await lstat3(absolute);
|
|
966
|
+
} catch (error) {
|
|
967
|
+
diagnostics.push({
|
|
968
|
+
code: "VERIFY_READ_FAILED",
|
|
969
|
+
destination: relative,
|
|
970
|
+
message: error instanceof Error ? error.message : String(error)
|
|
971
|
+
});
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
if (info.isSymbolicLink()) {
|
|
975
|
+
diagnostics.push({
|
|
976
|
+
code: "VERIFY_SYMLINK_DETECTED",
|
|
977
|
+
destination: relative,
|
|
978
|
+
message: `Generated output contains a symbolic link at "${relative}".`
|
|
979
|
+
});
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
if (info.isDirectory()) {
|
|
983
|
+
const canonical = await realpath2(absolute);
|
|
984
|
+
if (!isWithin(root, canonical)) {
|
|
985
|
+
diagnostics.push({
|
|
986
|
+
code: "VERIFY_TARGET_INVALID",
|
|
987
|
+
destination: relative,
|
|
988
|
+
message: `Directory "${relative}" resolves outside the output root.`
|
|
989
|
+
});
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
await walk2(absolute, relative);
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
if (!info.isFile()) {
|
|
996
|
+
diagnostics.push({
|
|
997
|
+
code: "VERIFY_FILE_TYPE_MISMATCH",
|
|
998
|
+
destination: relative,
|
|
999
|
+
message: `Generated path "${relative}" is not a regular file.`
|
|
1000
|
+
});
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
actual.add(relative);
|
|
1004
|
+
const operation = expected.get(relative);
|
|
1005
|
+
if (!operation) {
|
|
1006
|
+
if (options.rejectUnexpectedFiles !== false) {
|
|
1007
|
+
diagnostics.push({
|
|
1008
|
+
code: "VERIFY_UNEXPECTED_FILE",
|
|
1009
|
+
destination: relative,
|
|
1010
|
+
message: `Unexpected file "${relative}" exists in generated output.`
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
try {
|
|
1016
|
+
const content = await readFile(absolute);
|
|
1017
|
+
if (!sameContent(content, operation.content)) {
|
|
1018
|
+
diagnostics.push({
|
|
1019
|
+
code: "VERIFY_CONTENT_MISMATCH",
|
|
1020
|
+
destination: relative,
|
|
1021
|
+
message: `Generated file "${relative}" does not match the rendered execution plan.`
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
diagnostics.push({
|
|
1026
|
+
code: "VERIFY_READ_FAILED",
|
|
1027
|
+
destination: relative,
|
|
1028
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
await walk2(root, "");
|
|
1034
|
+
for (const destination of expected.keys()) {
|
|
1035
|
+
if (!actual.has(destination)) {
|
|
1036
|
+
diagnostics.push({
|
|
1037
|
+
code: "VERIFY_FILE_MISSING",
|
|
1038
|
+
destination,
|
|
1039
|
+
message: `Expected generated file "${destination}" is missing.`
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return Object.freeze({
|
|
1044
|
+
ok: diagnostics.length === 0,
|
|
1045
|
+
targetDirectory: root,
|
|
1046
|
+
expectedFiles: Object.freeze([...expected.keys()].sort()),
|
|
1047
|
+
actualFiles: Object.freeze([...actual].sort()),
|
|
1048
|
+
diagnostics: Object.freeze(diagnostics)
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// ../os/src/state/appinit-state.ts
|
|
1053
|
+
import { randomUUID } from "crypto";
|
|
1054
|
+
import { mkdir as mkdir2, readFile as readFile2, realpath as realpath3, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
1055
|
+
import { homedir } from "os";
|
|
1056
|
+
import path7 from "path";
|
|
1057
|
+
function isWithin2(parent, child) {
|
|
1058
|
+
const relative = path7.relative(parent, child);
|
|
1059
|
+
return relative === "" || !relative.startsWith(`..${path7.sep}`) && relative !== ".." && !path7.isAbsolute(relative);
|
|
1060
|
+
}
|
|
1061
|
+
function resolveAppInitHome(options = {}) {
|
|
1062
|
+
const env = options.env ?? process.env;
|
|
1063
|
+
const configuredHome = env.APPINIT_HOME;
|
|
1064
|
+
if (configuredHome !== void 0) {
|
|
1065
|
+
if (configuredHome.trim().length === 0) {
|
|
1066
|
+
throw new Error("APPINIT_HOME must not be empty when set.");
|
|
1067
|
+
}
|
|
1068
|
+
return path7.resolve(configuredHome);
|
|
1069
|
+
}
|
|
1070
|
+
return path7.resolve(options.homeDirectory ?? homedir(), ".appinit");
|
|
1071
|
+
}
|
|
1072
|
+
async function ensureAppInitHome(homeDirectory = resolveAppInitHome()) {
|
|
1073
|
+
const resolvedHome = path7.resolve(homeDirectory);
|
|
1074
|
+
await mkdir2(resolvedHome, { recursive: true, mode: 448 });
|
|
1075
|
+
return realpath3(resolvedHome);
|
|
1076
|
+
}
|
|
1077
|
+
function resolveAppInitPath(homeDirectory, relativePath) {
|
|
1078
|
+
if (typeof relativePath !== "string" || relativePath.length === 0 || relativePath.includes("\\") || relativePath.includes("\0") || relativePath.startsWith("/") || relativePath.split("/").some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
1079
|
+
throw new Error("AppInit state path must be a safe, non-empty relative path.");
|
|
1080
|
+
}
|
|
1081
|
+
const root = path7.resolve(homeDirectory);
|
|
1082
|
+
const target = path7.resolve(root, ...relativePath.split("/"));
|
|
1083
|
+
if (!isWithin2(root, target) || root === target) {
|
|
1084
|
+
throw new Error("AppInit state path must remain inside the global state directory.");
|
|
1085
|
+
}
|
|
1086
|
+
return target;
|
|
1087
|
+
}
|
|
1088
|
+
async function resolveExistingStateFile(homeDirectory, relativePath) {
|
|
1089
|
+
const candidate = resolveAppInitPath(homeDirectory, relativePath);
|
|
1090
|
+
const root = await realpath3(homeDirectory);
|
|
1091
|
+
const resolved = await realpath3(candidate);
|
|
1092
|
+
if (!isWithin2(root, resolved) || root === resolved) {
|
|
1093
|
+
throw new Error("AppInit state path resolves outside the global state directory.");
|
|
1094
|
+
}
|
|
1095
|
+
return resolved;
|
|
1096
|
+
}
|
|
1097
|
+
async function readAppInitJson(homeDirectory, relativePath) {
|
|
1098
|
+
let contents;
|
|
1099
|
+
try {
|
|
1100
|
+
const filePath = await resolveExistingStateFile(homeDirectory, relativePath);
|
|
1101
|
+
contents = await readFile2(filePath, "utf8");
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
if (error.code === "ENOENT") return void 0;
|
|
1104
|
+
throw error;
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
return JSON.parse(contents);
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
throw new Error("AppInit state file contains invalid JSON.", { cause: error });
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
async function ensureStateParent(homeDirectory, relativePath) {
|
|
1113
|
+
const root = await ensureAppInitHome(homeDirectory);
|
|
1114
|
+
const segments = relativePath.split("/");
|
|
1115
|
+
let current = root;
|
|
1116
|
+
for (const segment of segments.slice(0, -1)) {
|
|
1117
|
+
current = path7.join(current, segment);
|
|
1118
|
+
await mkdir2(current, { recursive: false, mode: 448 }).catch((error) => {
|
|
1119
|
+
if (error.code !== "EEXIST") throw error;
|
|
1120
|
+
});
|
|
1121
|
+
const resolved = await realpath3(current);
|
|
1122
|
+
if (!isWithin2(root, resolved) || root === resolved) {
|
|
1123
|
+
throw new Error("AppInit state path resolves outside the global state directory.");
|
|
1124
|
+
}
|
|
1125
|
+
current = resolved;
|
|
1126
|
+
}
|
|
1127
|
+
return current;
|
|
1128
|
+
}
|
|
1129
|
+
async function writeAppInitJson(homeDirectory, relativePath, value) {
|
|
1130
|
+
const candidate = resolveAppInitPath(homeDirectory, relativePath);
|
|
1131
|
+
const serialized = JSON.stringify(value, null, 2);
|
|
1132
|
+
if (serialized === void 0) {
|
|
1133
|
+
throw new Error("AppInit state value must be JSON-serializable.");
|
|
1134
|
+
}
|
|
1135
|
+
const directory = await ensureStateParent(homeDirectory, relativePath);
|
|
1136
|
+
const filePath = path7.join(directory, path7.basename(candidate));
|
|
1137
|
+
const temporaryPath = path7.join(directory, `.${path7.basename(filePath)}.${randomUUID()}.tmp`);
|
|
1138
|
+
try {
|
|
1139
|
+
await writeFile2(temporaryPath, `${serialized}
|
|
1140
|
+
`, {
|
|
1141
|
+
encoding: "utf8",
|
|
1142
|
+
flag: "wx",
|
|
1143
|
+
mode: 384
|
|
1144
|
+
});
|
|
1145
|
+
await rename2(temporaryPath, filePath);
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
await rm2(temporaryPath, { force: true }).catch(() => void 0);
|
|
1148
|
+
throw error;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// ../os/src/state/appinit-state-schema.ts
|
|
1153
|
+
function isRecord4(value) {
|
|
1154
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1155
|
+
}
|
|
1156
|
+
function isNonEmptyString2(value) {
|
|
1157
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1158
|
+
}
|
|
1159
|
+
function validateAppInitState(value) {
|
|
1160
|
+
const errors = [];
|
|
1161
|
+
if (!isRecord4(value)) {
|
|
1162
|
+
return { valid: false, errors: ["State must be a JSON object."] };
|
|
1163
|
+
}
|
|
1164
|
+
const allowedKeys = ["schemaVersion", "projects", "lastUpdateCheck", "latestVersion"];
|
|
1165
|
+
if (Object.keys(value).some((key) => !allowedKeys.includes(key))) {
|
|
1166
|
+
errors.push("State contains unknown fields.");
|
|
1167
|
+
}
|
|
1168
|
+
const isLegacyUpdateState = !Object.prototype.hasOwnProperty.call(value, "schemaVersion") && !Object.prototype.hasOwnProperty.call(value, "projects") && (Object.prototype.hasOwnProperty.call(value, "lastUpdateCheck") || Object.prototype.hasOwnProperty.call(value, "latestVersion"));
|
|
1169
|
+
const unsupportedVersion = !isLegacyUpdateState && typeof value.schemaVersion === "number" && Number.isInteger(value.schemaVersion) && value.schemaVersion > 1 ? value.schemaVersion : void 0;
|
|
1170
|
+
if (!isLegacyUpdateState && value.schemaVersion !== 1) {
|
|
1171
|
+
errors.push(unsupportedVersion === void 0 ? "schemaVersion must be 1." : `State uses unsupported newer schema version ${unsupportedVersion}; this AppInit version supports schema version 1.`);
|
|
1172
|
+
}
|
|
1173
|
+
const rawProjects = isLegacyUpdateState ? [] : value.projects;
|
|
1174
|
+
if (!Array.isArray(rawProjects)) {
|
|
1175
|
+
errors.push("projects must be an array.");
|
|
1176
|
+
} else {
|
|
1177
|
+
rawProjects.forEach((project, index) => {
|
|
1178
|
+
if (!isRecord4(project)) {
|
|
1179
|
+
errors.push(`projects[${index}] must be an object.`);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (Object.keys(project).some((key) => !["path", "templateId", "createdAt"].includes(key))) {
|
|
1183
|
+
errors.push(`projects[${index}] contains unknown fields.`);
|
|
1184
|
+
}
|
|
1185
|
+
if (!isNonEmptyString2(project.path)) {
|
|
1186
|
+
errors.push(`projects[${index}].path must be a non-empty string.`);
|
|
1187
|
+
}
|
|
1188
|
+
if (!isNonEmptyString2(project.templateId)) {
|
|
1189
|
+
errors.push(`projects[${index}].templateId must be a non-empty string.`);
|
|
1190
|
+
}
|
|
1191
|
+
if (!isNonEmptyString2(project.createdAt) || !Number.isFinite(Date.parse(project.createdAt))) {
|
|
1192
|
+
errors.push(`projects[${index}].createdAt must be a valid date string.`);
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
if (errors.length > 0) return { valid: false, errors, ...unsupportedVersion === void 0 ? {} : { unsupportedVersion } };
|
|
1197
|
+
return {
|
|
1198
|
+
valid: true,
|
|
1199
|
+
errors: [],
|
|
1200
|
+
state: {
|
|
1201
|
+
...value,
|
|
1202
|
+
schemaVersion: 1,
|
|
1203
|
+
projects: rawProjects
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/context.ts
|
|
1209
|
+
function isRecord5(value) {
|
|
1210
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1211
|
+
}
|
|
1212
|
+
async function readOptionalJson(filePath, diagnostics) {
|
|
1213
|
+
let source;
|
|
1214
|
+
try {
|
|
1215
|
+
source = await readFile3(filePath, "utf8");
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
if (error.code === "ENOENT") return void 0;
|
|
1218
|
+
diagnostics.push(`Could not read ${path8.basename(filePath)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1219
|
+
return void 0;
|
|
1220
|
+
}
|
|
1221
|
+
try {
|
|
1222
|
+
const value = JSON.parse(source);
|
|
1223
|
+
if (!isRecord5(value)) {
|
|
1224
|
+
diagnostics.push(`${path8.basename(filePath)} must contain a JSON object.`);
|
|
1225
|
+
return void 0;
|
|
1226
|
+
}
|
|
1227
|
+
return value;
|
|
1228
|
+
} catch {
|
|
1229
|
+
diagnostics.push(`${path8.basename(filePath)} contains invalid JSON.`);
|
|
1230
|
+
return void 0;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
function optionalString(record, key) {
|
|
1234
|
+
const value = record?.[key];
|
|
1235
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
1236
|
+
}
|
|
1237
|
+
function samePath(left, right) {
|
|
1238
|
+
const a = path8.resolve(left);
|
|
1239
|
+
const b = path8.resolve(right);
|
|
1240
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
1241
|
+
}
|
|
1242
|
+
async function readProjectContext(cwd = process.cwd()) {
|
|
1243
|
+
const rootDir = path8.resolve(cwd);
|
|
1244
|
+
const diagnostics = [];
|
|
1245
|
+
const appinitDirectory = path8.join(rootDir, ".appinit");
|
|
1246
|
+
const [packageJson, project, template, context] = await Promise.all([
|
|
1247
|
+
readOptionalJson(path8.join(rootDir, "package.json"), diagnostics),
|
|
1248
|
+
readOptionalJson(path8.join(appinitDirectory, "project.json"), diagnostics),
|
|
1249
|
+
readOptionalJson(path8.join(appinitDirectory, "template.json"), diagnostics),
|
|
1250
|
+
readOptionalJson(path8.join(appinitDirectory, "context.json"), diagnostics)
|
|
1251
|
+
]);
|
|
1252
|
+
let history;
|
|
1253
|
+
try {
|
|
1254
|
+
const value = await readAppInitJson(resolveAppInitHome(), "state.json");
|
|
1255
|
+
if (value !== void 0) {
|
|
1256
|
+
const validation = validateAppInitState(value);
|
|
1257
|
+
if (!validation.valid || !validation.state) {
|
|
1258
|
+
diagnostics.push(`Global AppInit state is invalid: ${validation.errors.join(" ")}`);
|
|
1259
|
+
} else {
|
|
1260
|
+
history = validation.state.projects.find((entry) => samePath(entry.path, rootDir));
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
diagnostics.push(`Global AppInit state could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
1265
|
+
}
|
|
1266
|
+
const packageInfo = packageJson ? {
|
|
1267
|
+
...optionalString(packageJson, "name") ? { name: optionalString(packageJson, "name") } : {},
|
|
1268
|
+
...optionalString(packageJson, "version") ? { version: optionalString(packageJson, "version") } : {}
|
|
1269
|
+
} : void 0;
|
|
1270
|
+
const appinit = project || template || context ? {
|
|
1271
|
+
...project ? { project } : {},
|
|
1272
|
+
...template ? { template } : {},
|
|
1273
|
+
...context ? { context } : {}
|
|
1274
|
+
} : void 0;
|
|
1275
|
+
return Object.freeze({
|
|
1276
|
+
rootDir,
|
|
1277
|
+
...packageInfo ? { package: Object.freeze(packageInfo) } : {},
|
|
1278
|
+
...appinit ? { appinit: Object.freeze(appinit) } : {},
|
|
1279
|
+
...history ? { history } : {},
|
|
1280
|
+
diagnostics: Object.freeze(diagnostics)
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
async function runContextCommand(options = {}, io = { write: (message) => process.stdout.write(message) }) {
|
|
1284
|
+
const report = await readProjectContext(options.cwd);
|
|
1285
|
+
if (options.json) {
|
|
1286
|
+
io.write(`${JSON.stringify(report, null, 2)}
|
|
1287
|
+
`);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
io.write("AppInit Project Context\n\n");
|
|
1291
|
+
io.write(`Project: ${report.package?.name ?? report.appinit?.project?.name ?? path8.basename(report.rootDir)}
|
|
1292
|
+
`);
|
|
1293
|
+
io.write(`Path: ${report.rootDir}
|
|
1294
|
+
`);
|
|
1295
|
+
if (report.package?.version) io.write(`Version: ${report.package.version}
|
|
1296
|
+
`);
|
|
1297
|
+
const project = report.appinit?.project;
|
|
1298
|
+
if (project) {
|
|
1299
|
+
io.write("\nProject metadata (.appinit/project.json)\n");
|
|
1300
|
+
for (const key of ["name", "createdAt", "cliVersion", "atsVersion"]) {
|
|
1301
|
+
const value = optionalString(project, key);
|
|
1302
|
+
if (value) io.write(` ${key}: ${value}
|
|
1303
|
+
`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
const template = report.appinit?.template;
|
|
1307
|
+
if (template) {
|
|
1308
|
+
io.write("\nTemplate metadata (.appinit/template.json)\n");
|
|
1309
|
+
for (const key of ["templateId", "version", "source", "installedAt"]) {
|
|
1310
|
+
const value = optionalString(template, key);
|
|
1311
|
+
if (value) io.write(` ${key}: ${value}
|
|
1312
|
+
`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const context = report.appinit?.context;
|
|
1316
|
+
if (context) {
|
|
1317
|
+
io.write("\nDeclared context (.appinit/context.json)\n");
|
|
1318
|
+
for (const key of ["artifactKind", "runtime", "framework", "atsVersion"]) {
|
|
1319
|
+
const value = optionalString(context, key);
|
|
1320
|
+
if (value) io.write(` ${key}: ${value}
|
|
1321
|
+
`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (report.history) {
|
|
1325
|
+
io.write("\nGlobal project history\n");
|
|
1326
|
+
io.write(` Template: ${report.history.templateId}
|
|
1327
|
+
`);
|
|
1328
|
+
io.write(` Created: ${report.history.createdAt}
|
|
1329
|
+
`);
|
|
1330
|
+
} else {
|
|
1331
|
+
io.write("\nGlobal project history: no matching entry\n");
|
|
1332
|
+
}
|
|
1333
|
+
if (!report.appinit) io.write("\nNo .appinit project metadata found.\n");
|
|
1334
|
+
if (report.diagnostics.length) {
|
|
1335
|
+
io.write("\nDiagnostics\n");
|
|
1336
|
+
for (const diagnostic3 of report.diagnostics) io.write(` ! ${diagnostic3}
|
|
1337
|
+
`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// src/create.ts
|
|
1342
|
+
import { lstat as lstat6, readdir as readdir4, readFile as readFile9, realpath as realpath7 } from "fs/promises";
|
|
1343
|
+
import path16 from "path";
|
|
1344
|
+
|
|
1345
|
+
// ../sdk/src/resolver/resolve-template.ts
|
|
1346
|
+
import { realpath as realpath4, stat, readFile as readFile5 } from "fs/promises";
|
|
1347
|
+
import path10 from "path";
|
|
1348
|
+
|
|
1349
|
+
// ../sdk/src/resolver/template-vfs.ts
|
|
1350
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1351
|
+
import path9 from "path";
|
|
1352
|
+
var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([".git", "node_modules", "docs", "variables", "hooks"]);
|
|
1353
|
+
var STATIC_RENAMES = {
|
|
1354
|
+
_gitignore: ".gitignore",
|
|
1355
|
+
_npmrc: ".npmrc"
|
|
1356
|
+
};
|
|
1357
|
+
function normalizeTemplatePath(relativePath) {
|
|
1358
|
+
let dest = relativePath.replace(/\\/g, "/");
|
|
1359
|
+
let render = false;
|
|
1360
|
+
if (dest.endsWith("__tmpl")) {
|
|
1361
|
+
dest = dest.slice(0, -"__tmpl".length);
|
|
1362
|
+
render = true;
|
|
1363
|
+
} else if (dest.endsWith(".tmpl")) {
|
|
1364
|
+
dest = dest.slice(0, -".tmpl".length);
|
|
1365
|
+
render = true;
|
|
1366
|
+
}
|
|
1367
|
+
const segments = dest.split("/");
|
|
1368
|
+
const basename = segments.pop() ?? "";
|
|
1369
|
+
const renamed = STATIC_RENAMES[basename] ?? basename;
|
|
1370
|
+
const normalized = [...segments, renamed].filter(Boolean).join("/");
|
|
1371
|
+
return { dest: normalized, render };
|
|
1372
|
+
}
|
|
1373
|
+
async function createTemplateVFS(templateDirectory) {
|
|
1374
|
+
const root = await import("fs/promises").then(({ realpath: realpath9 }) => realpath9(templateDirectory));
|
|
1375
|
+
const entries = await readDirRecursive(root);
|
|
1376
|
+
const vfs = new VirtualFileSystem();
|
|
1377
|
+
for (const relativePath of entries) {
|
|
1378
|
+
const segments = relativePath.split("/");
|
|
1379
|
+
if (segments.some((segment) => IGNORED_SEGMENTS.has(segment)) || segments.at(-1) === ".DS_Store") continue;
|
|
1380
|
+
const { dest, render } = normalizeTemplatePath(relativePath);
|
|
1381
|
+
const absolutePath = path9.join(root, ...relativePath.split("/"));
|
|
1382
|
+
const binary = isBinaryFile(absolutePath);
|
|
1383
|
+
const content = binary ? await readFile4(absolutePath) : await readFile4(absolutePath, "utf8");
|
|
1384
|
+
vfs.add({ dest, content, render, binary });
|
|
1385
|
+
}
|
|
1386
|
+
return freezeVFS(vfs);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// ../sdk/src/resolver/resolve-template.ts
|
|
1390
|
+
function isWithin3(parent, child) {
|
|
1391
|
+
const relative = path10.relative(parent, child);
|
|
1392
|
+
return relative === "" || !relative.startsWith(`..${path10.sep}`) && relative !== ".." && !path10.isAbsolute(relative);
|
|
1393
|
+
}
|
|
1394
|
+
async function resolveTemplate(templateRoot, options = {}) {
|
|
1395
|
+
const root = await realpath4(path10.resolve(templateRoot));
|
|
1396
|
+
const rootInfo = await stat(root);
|
|
1397
|
+
if (!rootInfo.isDirectory()) {
|
|
1398
|
+
throw new Error(`Template root is not a directory: ${templateRoot}`);
|
|
1399
|
+
}
|
|
1400
|
+
const manifestFile = options.manifestFile ?? "appinit.template.json";
|
|
1401
|
+
if (path10.isAbsolute(manifestFile) || manifestFile.includes("\\") || manifestFile.split("/").some((part) => part === ".." || part === "." || part === "")) {
|
|
1402
|
+
throw new Error("Manifest filename must be a safe relative path.");
|
|
1403
|
+
}
|
|
1404
|
+
const manifestPath = await realpath4(path10.join(root, manifestFile));
|
|
1405
|
+
if (!isWithin3(root, manifestPath)) {
|
|
1406
|
+
throw new Error("Template manifest resolves outside the template root.");
|
|
1407
|
+
}
|
|
1408
|
+
const raw = await readFile5(manifestPath, "utf8");
|
|
1409
|
+
let value;
|
|
1410
|
+
try {
|
|
1411
|
+
value = JSON.parse(raw);
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
throw new Error(`Template manifest contains invalid JSON: ${manifestPath}`, { cause: error });
|
|
1414
|
+
}
|
|
1415
|
+
const validation = validateTemplateManifest(value);
|
|
1416
|
+
if (!validation.valid || !validation.manifest) {
|
|
1417
|
+
throw new Error(`Invalid template manifest: ${validation.errors.join(" ")}`);
|
|
1418
|
+
}
|
|
1419
|
+
const declaredTemplatePath = path10.resolve(root, ...validation.manifest.paths.template.split("/"));
|
|
1420
|
+
const templatePath = await realpath4(declaredTemplatePath);
|
|
1421
|
+
if (!isWithin3(root, templatePath)) {
|
|
1422
|
+
throw new Error("Declared template path resolves outside the template root.");
|
|
1423
|
+
}
|
|
1424
|
+
const templateInfo = await stat(templatePath);
|
|
1425
|
+
if (!templateInfo.isDirectory()) {
|
|
1426
|
+
throw new Error("Declared template path is not a directory.");
|
|
1427
|
+
}
|
|
1428
|
+
const vfs = await createTemplateVFS(templatePath);
|
|
1429
|
+
return Object.freeze({ root, manifest: validation.manifest, vfs });
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// ../sdk/src/resolver/discover-template.ts
|
|
1433
|
+
async function discoverTemplate(templateRef, options) {
|
|
1434
|
+
if (typeof templateRef !== "string" || templateRef.trim().length === 0) {
|
|
1435
|
+
throw new Error("Template reference must be a non-empty string.");
|
|
1436
|
+
}
|
|
1437
|
+
const root = options.templateRoots.get(templateRef);
|
|
1438
|
+
if (!root) {
|
|
1439
|
+
throw new Error(`Template "${templateRef}" is not present in the approved template roots.`);
|
|
1440
|
+
}
|
|
1441
|
+
const template = await resolveTemplate(root, {
|
|
1442
|
+
...options.manifestFile ? { manifestFile: options.manifestFile } : {}
|
|
1443
|
+
});
|
|
1444
|
+
const manifest = template.manifest;
|
|
1445
|
+
if (manifest.id === templateRef) {
|
|
1446
|
+
return Object.freeze({ template, matchedBy: "id" });
|
|
1447
|
+
}
|
|
1448
|
+
if (manifest.packageName === templateRef) {
|
|
1449
|
+
return Object.freeze({ template, matchedBy: "packageName" });
|
|
1450
|
+
}
|
|
1451
|
+
throw new Error(
|
|
1452
|
+
`Approved root for "${templateRef}" contains template "${manifest.id}" (${manifest.packageName}); identity mismatch.`
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// ../sdk/src/resolver/discover-template-catalog.ts
|
|
1457
|
+
import { lstat as lstat4, readdir as readdir3, realpath as realpath5 } from "fs/promises";
|
|
1458
|
+
import path11 from "path";
|
|
1459
|
+
|
|
1460
|
+
// ../sdk/src/resolver/variables.ts
|
|
1461
|
+
import { realpath as realpath6, readFile as readFile6, stat as stat2 } from "fs/promises";
|
|
1462
|
+
import path12 from "path";
|
|
1463
|
+
function isRecord6(value) {
|
|
1464
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1465
|
+
}
|
|
1466
|
+
function isWithin4(parent, child) {
|
|
1467
|
+
const relative = path12.relative(parent, child);
|
|
1468
|
+
return relative === "" || !relative.startsWith(`..${path12.sep}`) && relative !== ".." && !path12.isAbsolute(relative);
|
|
1469
|
+
}
|
|
1470
|
+
async function readJsonRecord(filePath) {
|
|
1471
|
+
let value;
|
|
1472
|
+
try {
|
|
1473
|
+
value = JSON.parse(await readFile6(filePath, "utf8"));
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
if (error.code === "ENOENT") throw error;
|
|
1476
|
+
throw new Error(`Unable to parse template variable file: ${filePath}`, { cause: error });
|
|
1477
|
+
}
|
|
1478
|
+
if (!isRecord6(value)) throw new Error(`Template variable file must contain a JSON object: ${filePath}`);
|
|
1479
|
+
return value;
|
|
1480
|
+
}
|
|
1481
|
+
async function loadTemplateVariables(templateRoot, manifest) {
|
|
1482
|
+
if (!manifest.paths.variables) return { defaults: {}, schema: {} };
|
|
1483
|
+
const root = await realpath6(path12.resolve(templateRoot));
|
|
1484
|
+
const directory = path12.resolve(root, ...manifest.paths.variables.split("/"));
|
|
1485
|
+
let variablesRoot;
|
|
1486
|
+
try {
|
|
1487
|
+
variablesRoot = await realpath6(directory);
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
if (error.code === "ENOENT") return { defaults: {}, schema: {} };
|
|
1490
|
+
throw error;
|
|
1491
|
+
}
|
|
1492
|
+
if (!isWithin4(root, variablesRoot)) {
|
|
1493
|
+
throw new Error("Declared variables directory resolves outside the template root.");
|
|
1494
|
+
}
|
|
1495
|
+
if (!(await stat2(variablesRoot)).isDirectory()) {
|
|
1496
|
+
throw new Error("Declared variables path is not a directory.");
|
|
1497
|
+
}
|
|
1498
|
+
const defaultsPath = path12.join(variablesRoot, "defaults.json");
|
|
1499
|
+
const schemaPath = path12.join(variablesRoot, "schema.json");
|
|
1500
|
+
const defaults = await readOptionalJson2(defaultsPath);
|
|
1501
|
+
const schemaRaw = await readOptionalJson2(schemaPath);
|
|
1502
|
+
const schema = {};
|
|
1503
|
+
for (const [key, rule] of Object.entries(schemaRaw)) {
|
|
1504
|
+
if (!isRecord6(rule)) throw new Error(`Variable schema entry "${key}" must be an object.`);
|
|
1505
|
+
if (rule.type !== void 0 && !["string", "number", "boolean", "object", "array"].includes(String(rule.type))) {
|
|
1506
|
+
throw new Error(`Variable schema entry "${key}" has an unsupported type.`);
|
|
1507
|
+
}
|
|
1508
|
+
if (rule.required !== void 0 && typeof rule.required !== "boolean") {
|
|
1509
|
+
throw new Error(`Variable schema entry "${key}".required must be boolean.`);
|
|
1510
|
+
}
|
|
1511
|
+
if (rule.minLength !== void 0 && (!Number.isInteger(rule.minLength) || Number(rule.minLength) < 0)) {
|
|
1512
|
+
throw new Error(`Variable schema entry "${key}".minLength must be a non-negative integer.`);
|
|
1513
|
+
}
|
|
1514
|
+
if (rule.enum !== void 0 && !Array.isArray(rule.enum)) {
|
|
1515
|
+
throw new Error(`Variable schema entry "${key}".enum must be an array.`);
|
|
1516
|
+
}
|
|
1517
|
+
schema[key] = rule;
|
|
1518
|
+
}
|
|
1519
|
+
return Object.freeze({
|
|
1520
|
+
defaults: Object.freeze(defaults),
|
|
1521
|
+
schema: Object.freeze(schema)
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
async function readOptionalJson2(filePath) {
|
|
1525
|
+
try {
|
|
1526
|
+
return await readJsonRecord(filePath);
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
if (error.code === "ENOENT") return {};
|
|
1529
|
+
throw error;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function validateTemplateVariables2(values, schema) {
|
|
1533
|
+
const errors = [];
|
|
1534
|
+
for (const [key, rule] of Object.entries(schema)) {
|
|
1535
|
+
const value = values[key];
|
|
1536
|
+
if (value === void 0 || value === null) {
|
|
1537
|
+
if (rule.required) errors.push(`Variable "${key}" is required.`);
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (rule.type) {
|
|
1541
|
+
const valid = rule.type === "array" ? Array.isArray(value) : rule.type === "object" ? isRecord6(value) : typeof value === rule.type;
|
|
1542
|
+
if (!valid || rule.type === "number" && !Number.isFinite(value)) {
|
|
1543
|
+
errors.push(`Variable "${key}" must be a valid ${rule.type}.`);
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (rule.minLength !== void 0 && typeof value === "string" && value.length < rule.minLength) {
|
|
1548
|
+
errors.push(`Variable "${key}" must contain at least ${rule.minLength} characters.`);
|
|
1549
|
+
}
|
|
1550
|
+
if (rule.enum && !rule.enum.some((allowed) => Object.is(allowed, value))) {
|
|
1551
|
+
errors.push(`Variable "${key}" must be one of: ${rule.enum.map(String).join(", ")}.`);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return errors;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// ../sdk/src/resolver/template-registry-client.ts
|
|
1558
|
+
import { createHash, randomUUID as randomUUID2, timingSafeEqual } from "crypto";
|
|
1559
|
+
import { mkdir as mkdir3, readFile as readFile7, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
1560
|
+
import path13 from "path";
|
|
1561
|
+
var DEFAULT_MAX_CATALOG_BYTES = 2 * 1024 * 1024;
|
|
1562
|
+
var DEFAULT_MAX_ARCHIVE_BYTES = 100 * 1024 * 1024;
|
|
1563
|
+
function assertHttpsUrl(value, label) {
|
|
1564
|
+
const url = new URL(value);
|
|
1565
|
+
if (url.protocol !== "https:" || url.username || url.password || url.hash) {
|
|
1566
|
+
throw new Error(`${label} must be an HTTPS URL without credentials or a fragment.`);
|
|
1567
|
+
}
|
|
1568
|
+
return url;
|
|
1569
|
+
}
|
|
1570
|
+
function safeSegment(value, label) {
|
|
1571
|
+
if (!/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(value)) {
|
|
1572
|
+
throw new Error(`Invalid ${label} for registry cache path.`);
|
|
1573
|
+
}
|
|
1574
|
+
return value;
|
|
1575
|
+
}
|
|
1576
|
+
async function readBoundedResponse(response, maxBytes, label) {
|
|
1577
|
+
if (!response.ok) throw new Error(`${label} request failed with HTTP ${response.status}.`);
|
|
1578
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
1579
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
1580
|
+
throw new Error(`${label} exceeds the ${maxBytes}-byte size limit.`);
|
|
1581
|
+
}
|
|
1582
|
+
if (!response.body) return new Uint8Array();
|
|
1583
|
+
const reader = response.body.getReader();
|
|
1584
|
+
const chunks = [];
|
|
1585
|
+
let total = 0;
|
|
1586
|
+
try {
|
|
1587
|
+
while (true) {
|
|
1588
|
+
const { done, value } = await reader.read();
|
|
1589
|
+
if (done) break;
|
|
1590
|
+
total += value.byteLength;
|
|
1591
|
+
if (total > maxBytes) {
|
|
1592
|
+
await reader.cancel();
|
|
1593
|
+
throw new Error(`${label} exceeds the ${maxBytes}-byte size limit.`);
|
|
1594
|
+
}
|
|
1595
|
+
chunks.push(value);
|
|
1596
|
+
}
|
|
1597
|
+
} finally {
|
|
1598
|
+
reader.releaseLock();
|
|
1599
|
+
}
|
|
1600
|
+
const bytes = new Uint8Array(total);
|
|
1601
|
+
let offset = 0;
|
|
1602
|
+
for (const chunk of chunks) {
|
|
1603
|
+
bytes.set(chunk, offset);
|
|
1604
|
+
offset += chunk.byteLength;
|
|
1605
|
+
}
|
|
1606
|
+
return bytes;
|
|
1607
|
+
}
|
|
1608
|
+
function sha256(bytes) {
|
|
1609
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
1610
|
+
}
|
|
1611
|
+
function matchesDigest(actual, expected) {
|
|
1612
|
+
const actualBytes = Buffer.from(actual, "hex");
|
|
1613
|
+
const expectedBytes = Buffer.from(expected, "hex");
|
|
1614
|
+
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
|
|
1615
|
+
}
|
|
1616
|
+
var TemplateRegistryClient = class {
|
|
1617
|
+
constructor(options) {
|
|
1618
|
+
this.options = options;
|
|
1619
|
+
assertHttpsUrl(options.catalogUrl, "Catalog URL");
|
|
1620
|
+
this.request = options.fetch ?? fetch;
|
|
1621
|
+
this.maxCatalogBytes = options.maxCatalogBytes ?? DEFAULT_MAX_CATALOG_BYTES;
|
|
1622
|
+
this.maxArchiveBytes = options.maxArchiveBytes ?? DEFAULT_MAX_ARCHIVE_BYTES;
|
|
1623
|
+
}
|
|
1624
|
+
request;
|
|
1625
|
+
maxCatalogBytes;
|
|
1626
|
+
maxArchiveBytes;
|
|
1627
|
+
async fetchCatalog() {
|
|
1628
|
+
const response = await this.request(this.options.catalogUrl, {
|
|
1629
|
+
method: "GET",
|
|
1630
|
+
redirect: "error",
|
|
1631
|
+
headers: { accept: "application/json" }
|
|
1632
|
+
});
|
|
1633
|
+
const bytes = await readBoundedResponse(response, this.maxCatalogBytes, "Registry catalog");
|
|
1634
|
+
let parsed;
|
|
1635
|
+
try {
|
|
1636
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
1637
|
+
} catch {
|
|
1638
|
+
throw new Error("Registry catalog response is not valid UTF-8 JSON.");
|
|
1639
|
+
}
|
|
1640
|
+
const result = validateTemplateRegistryCatalog(parsed);
|
|
1641
|
+
if (!result.valid || !result.catalog) {
|
|
1642
|
+
throw new Error(`Registry catalog validation failed: ${result.errors.join("; ")}`);
|
|
1643
|
+
}
|
|
1644
|
+
return result.catalog;
|
|
1645
|
+
}
|
|
1646
|
+
async downloadTemplate(entry) {
|
|
1647
|
+
safeSegment(entry.id, "template ID");
|
|
1648
|
+
safeSegment(entry.version, "template version");
|
|
1649
|
+
const archiveUrl = assertHttpsUrl(entry.archive.url, "Template archive URL");
|
|
1650
|
+
const cacheRoot = path13.resolve(this.options.cacheDirectory);
|
|
1651
|
+
const destination = path13.resolve(cacheRoot, entry.id, entry.version, "template.tar");
|
|
1652
|
+
if (!destination.startsWith(cacheRoot + path13.sep)) {
|
|
1653
|
+
throw new Error("Resolved template cache path escapes the configured cache directory.");
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
const existing = await readFile7(destination);
|
|
1657
|
+
const digest2 = sha256(existing);
|
|
1658
|
+
if (matchesDigest(digest2, entry.archive.sha256)) {
|
|
1659
|
+
return Object.freeze({ entry, path: destination, sha256: digest2, cached: true });
|
|
1660
|
+
}
|
|
1661
|
+
await rm3(destination, { force: true });
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
if (error.code !== "ENOENT") throw error;
|
|
1664
|
+
}
|
|
1665
|
+
const response = await this.request(archiveUrl, {
|
|
1666
|
+
method: "GET",
|
|
1667
|
+
redirect: "error",
|
|
1668
|
+
headers: { accept: "application/octet-stream, application/gzip" }
|
|
1669
|
+
});
|
|
1670
|
+
const bytes = await readBoundedResponse(response, this.maxArchiveBytes, "Template archive");
|
|
1671
|
+
const digest = sha256(bytes);
|
|
1672
|
+
if (!matchesDigest(digest, entry.archive.sha256)) {
|
|
1673
|
+
throw new Error(`Template archive SHA-256 mismatch for ${entry.id}@${entry.version}.`);
|
|
1674
|
+
}
|
|
1675
|
+
await mkdir3(path13.dirname(destination), { recursive: true });
|
|
1676
|
+
const temporary = `${destination}.${randomUUID2()}.tmp`;
|
|
1677
|
+
try {
|
|
1678
|
+
await writeFile3(temporary, bytes, { flag: "wx", mode: 384 });
|
|
1679
|
+
await rename3(temporary, destination);
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
await rm3(temporary, { force: true });
|
|
1682
|
+
throw error;
|
|
1683
|
+
}
|
|
1684
|
+
return Object.freeze({ entry, path: destination, sha256: digest, cached: false });
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
// ../sdk/src/resolver/extract-template-archive.ts
|
|
1689
|
+
import { lstat as lstat5, mkdir as mkdir4, open, readFile as readFile8, rename as rename4, rm as rm4 } from "fs/promises";
|
|
1690
|
+
import path14 from "path";
|
|
1691
|
+
import { gunzipSync } from "zlib";
|
|
1692
|
+
var BLOCK_SIZE = 512;
|
|
1693
|
+
var DEFAULT_MAX_ENTRIES = 1e4;
|
|
1694
|
+
var DEFAULT_MAX_EXPANDED_BYTES = 250 * 1024 * 1024;
|
|
1695
|
+
var FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set([".", ".."]);
|
|
1696
|
+
function readString(header, start, length) {
|
|
1697
|
+
const end = header.indexOf(0, start);
|
|
1698
|
+
const stop = end === -1 || end >= start + length ? start + length : end;
|
|
1699
|
+
return header.toString("utf8", start, stop);
|
|
1700
|
+
}
|
|
1701
|
+
function readOctal(header, start, length, field) {
|
|
1702
|
+
const raw = header.toString("ascii", start, start + length).replace(/\0.*$/, "").trim();
|
|
1703
|
+
if (raw === "") return 0;
|
|
1704
|
+
if (!/^[0-7]+$/.test(raw)) throw new Error(`Invalid tar ${field} field.`);
|
|
1705
|
+
const value = Number.parseInt(raw, 8);
|
|
1706
|
+
if (!Number.isSafeInteger(value)) throw new Error(`Tar ${field} exceeds supported size.`);
|
|
1707
|
+
return value;
|
|
1708
|
+
}
|
|
1709
|
+
function verifyHeaderChecksum(header) {
|
|
1710
|
+
const expected = readOctal(header, 148, 8, "checksum");
|
|
1711
|
+
let actual = 0;
|
|
1712
|
+
for (let index = 0; index < BLOCK_SIZE; index++) {
|
|
1713
|
+
actual += index >= 148 && index < 156 ? 32 : header[index];
|
|
1714
|
+
}
|
|
1715
|
+
if (actual !== expected) throw new Error("Tar header checksum mismatch.");
|
|
1716
|
+
}
|
|
1717
|
+
function normalizeArchivePath(raw) {
|
|
1718
|
+
if (!raw || raw.includes("\0") || raw.includes("\\")) {
|
|
1719
|
+
throw new Error("Archive contains an empty or unsafe path.");
|
|
1720
|
+
}
|
|
1721
|
+
if (raw.startsWith("/") || /^[a-zA-Z]:/.test(raw) || raw.startsWith("//")) {
|
|
1722
|
+
throw new Error(`Archive path must be relative: ${raw}`);
|
|
1723
|
+
}
|
|
1724
|
+
const segments = raw.split("/");
|
|
1725
|
+
if (segments.some((segment) => FORBIDDEN_SEGMENTS.has(segment) || segment === "")) {
|
|
1726
|
+
throw new Error(`Archive path contains traversal or empty segments: ${raw}`);
|
|
1727
|
+
}
|
|
1728
|
+
return segments.join("/");
|
|
1729
|
+
}
|
|
1730
|
+
function isZeroBlock(block) {
|
|
1731
|
+
return block.every((byte) => byte === 0);
|
|
1732
|
+
}
|
|
1733
|
+
function parseTar(bytes) {
|
|
1734
|
+
const entries = [];
|
|
1735
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1736
|
+
let offset = 0;
|
|
1737
|
+
let zeroBlocks = 0;
|
|
1738
|
+
while (offset + BLOCK_SIZE <= bytes.length) {
|
|
1739
|
+
const header = bytes.subarray(offset, offset + BLOCK_SIZE);
|
|
1740
|
+
offset += BLOCK_SIZE;
|
|
1741
|
+
if (isZeroBlock(header)) {
|
|
1742
|
+
zeroBlocks++;
|
|
1743
|
+
if (zeroBlocks === 2) break;
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
if (zeroBlocks > 0) throw new Error("Tar archive has data after its end marker.");
|
|
1747
|
+
verifyHeaderChecksum(header);
|
|
1748
|
+
const name = readString(header, 0, 100);
|
|
1749
|
+
const prefix = readString(header, 345, 155);
|
|
1750
|
+
const rawPath = prefix ? `${prefix}/${name}` : name;
|
|
1751
|
+
const typeFlag = header[156];
|
|
1752
|
+
const size = readOctal(header, 124, 12, "size");
|
|
1753
|
+
const mode = readOctal(header, 100, 8, "mode");
|
|
1754
|
+
if ((mode & 3072) !== 0) throw new Error("Tar entries may not set setuid or setgid permissions.");
|
|
1755
|
+
if (typeFlag !== 0 && typeFlag !== 48 && typeFlag !== 53) {
|
|
1756
|
+
throw new Error("Tar archive contains a link, extension, or unsupported entry type.");
|
|
1757
|
+
}
|
|
1758
|
+
const type = typeFlag === 53 ? "directory" : "file";
|
|
1759
|
+
if (type === "directory" && size !== 0) throw new Error("Tar directory entries must be empty.");
|
|
1760
|
+
const normalized = normalizeArchivePath(type === "directory" && rawPath.endsWith("/") ? rawPath.slice(0, -1) : rawPath);
|
|
1761
|
+
const key = process.platform === "win32" ? normalized.toLocaleLowerCase("en-US") : normalized;
|
|
1762
|
+
if (seen.has(key)) throw new Error(`Tar archive contains duplicate path: ${normalized}`);
|
|
1763
|
+
seen.add(key);
|
|
1764
|
+
const paddedSize = Math.ceil(size / BLOCK_SIZE) * BLOCK_SIZE;
|
|
1765
|
+
if (offset + paddedSize > bytes.length) throw new Error(`Tar entry is truncated: ${normalized}`);
|
|
1766
|
+
entries.push({
|
|
1767
|
+
path: normalized,
|
|
1768
|
+
type,
|
|
1769
|
+
data: type === "file" ? bytes.subarray(offset, offset + size) : Buffer.alloc(0)
|
|
1770
|
+
});
|
|
1771
|
+
offset += paddedSize;
|
|
1772
|
+
}
|
|
1773
|
+
if (zeroBlocks < 2) throw new Error("Tar archive is missing its end marker.");
|
|
1774
|
+
return entries;
|
|
1775
|
+
}
|
|
1776
|
+
function isWithin5(parent, child) {
|
|
1777
|
+
const relative = path14.relative(parent, child);
|
|
1778
|
+
return relative === "" || !relative.startsWith(`..${path14.sep}`) && relative !== ".." && !path14.isAbsolute(relative);
|
|
1779
|
+
}
|
|
1780
|
+
async function extractTemplateArchive(options) {
|
|
1781
|
+
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
1782
|
+
const maxExpandedBytes = options.maxExpandedBytes ?? DEFAULT_MAX_EXPANDED_BYTES;
|
|
1783
|
+
if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) throw new Error("maxEntries must be a positive safe integer.");
|
|
1784
|
+
if (!Number.isSafeInteger(maxExpandedBytes) || maxExpandedBytes < 1) throw new Error("maxExpandedBytes must be a positive safe integer.");
|
|
1785
|
+
const archive = await readFile8(options.archivePath);
|
|
1786
|
+
let tarBytes;
|
|
1787
|
+
try {
|
|
1788
|
+
tarBytes = archive.length >= 2 && archive[0] === 31 && archive[1] === 139 ? gunzipSync(archive, { maxOutputLength: maxExpandedBytes + maxEntries * BLOCK_SIZE * 2 }) : archive;
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
throw new Error("Unable to decompress template archive within the configured limit.", { cause: error });
|
|
1791
|
+
}
|
|
1792
|
+
const entries = parseTar(tarBytes);
|
|
1793
|
+
if (entries.length > maxEntries) throw new Error(`Archive exceeds the ${maxEntries}-entry limit.`);
|
|
1794
|
+
let expandedBytes = 0;
|
|
1795
|
+
for (const entry of entries) {
|
|
1796
|
+
expandedBytes += entry.data.byteLength;
|
|
1797
|
+
if (expandedBytes > maxExpandedBytes) {
|
|
1798
|
+
throw new Error(`Archive exceeds the ${maxExpandedBytes}-byte expanded-size limit.`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
const destination = path14.resolve(options.destination);
|
|
1802
|
+
const parent = path14.dirname(destination);
|
|
1803
|
+
await mkdir4(parent, { recursive: true });
|
|
1804
|
+
const stage = path14.join(parent, `.appinit-extract-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
1805
|
+
if (!isWithin5(parent, stage) || stage === destination) throw new Error("Invalid extraction staging path.");
|
|
1806
|
+
let files = 0;
|
|
1807
|
+
let directories = 0;
|
|
1808
|
+
try {
|
|
1809
|
+
await mkdir4(stage, { recursive: false, mode: 448 });
|
|
1810
|
+
for (const entry of entries) {
|
|
1811
|
+
const target = path14.resolve(stage, ...entry.path.split("/"));
|
|
1812
|
+
if (!isWithin5(stage, target) || target === stage) throw new Error("Archive entry escapes extraction directory.");
|
|
1813
|
+
const relative = path14.relative(stage, target);
|
|
1814
|
+
const segments = relative.split(path14.sep);
|
|
1815
|
+
let current = stage;
|
|
1816
|
+
for (const segment of segments.slice(0, -1)) {
|
|
1817
|
+
current = path14.join(current, segment);
|
|
1818
|
+
try {
|
|
1819
|
+
const info = await lstat5(current);
|
|
1820
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Archive path traverses a non-directory entry.");
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
if (error.code !== "ENOENT") throw error;
|
|
1823
|
+
await mkdir4(current, { mode: 448 });
|
|
1824
|
+
directories++;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
if (entry.type === "directory") {
|
|
1828
|
+
try {
|
|
1829
|
+
await mkdir4(target, { mode: 448 });
|
|
1830
|
+
directories++;
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
if (error.code !== "EEXIST") throw error;
|
|
1833
|
+
const info = await lstat5(target);
|
|
1834
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Archive directory conflicts with a file.");
|
|
1835
|
+
}
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
const handle = await open(target, "wx", 384);
|
|
1839
|
+
try {
|
|
1840
|
+
await handle.writeFile(entry.data);
|
|
1841
|
+
} finally {
|
|
1842
|
+
await handle.close();
|
|
1843
|
+
}
|
|
1844
|
+
files++;
|
|
1845
|
+
}
|
|
1846
|
+
try {
|
|
1847
|
+
await lstat5(destination);
|
|
1848
|
+
throw new Error("Extraction destination already exists.");
|
|
1849
|
+
} catch (error) {
|
|
1850
|
+
if (error.code !== "ENOENT") throw error;
|
|
1851
|
+
}
|
|
1852
|
+
await rename4(stage, destination);
|
|
1853
|
+
return Object.freeze({ destination, files, directories, expandedBytes });
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
await rm4(stage, { recursive: true, force: true });
|
|
1856
|
+
throw error;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
// ../sdk/src/composition/resolve-composition.ts
|
|
1861
|
+
function stableUnique(values) {
|
|
1862
|
+
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
1863
|
+
}
|
|
1864
|
+
function addDiagnostic(diagnostics, diagnostic3) {
|
|
1865
|
+
diagnostics.push(diagnostic3);
|
|
1866
|
+
}
|
|
1867
|
+
function topologicalOrder(ids, edges) {
|
|
1868
|
+
const original = new Map(ids.map((id, index) => [id, index]));
|
|
1869
|
+
const indegree = new Map(ids.map((id) => [id, 0]));
|
|
1870
|
+
const outgoing = new Map(ids.map((id) => [id, /* @__PURE__ */ new Set()]));
|
|
1871
|
+
for (const edge of edges) {
|
|
1872
|
+
if (!outgoing.get(edge.to).has(edge.from)) {
|
|
1873
|
+
outgoing.get(edge.to).add(edge.from);
|
|
1874
|
+
indegree.set(edge.from, indegree.get(edge.from) + 1);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
const queue = ids.filter((id) => indegree.get(id) === 0).sort((a, b) => original.get(a) - original.get(b));
|
|
1878
|
+
const result = [];
|
|
1879
|
+
while (queue.length > 0) {
|
|
1880
|
+
const id = queue.shift();
|
|
1881
|
+
result.push(id);
|
|
1882
|
+
for (const next of outgoing.get(id)) {
|
|
1883
|
+
indegree.set(next, indegree.get(next) - 1);
|
|
1884
|
+
if (indegree.get(next) === 0) {
|
|
1885
|
+
queue.push(next);
|
|
1886
|
+
queue.sort((a, b) => original.get(a) - original.get(b));
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return result.length === ids.length ? result : void 0;
|
|
1891
|
+
}
|
|
1892
|
+
function resolveComposition(selected) {
|
|
1893
|
+
const diagnostics = [];
|
|
1894
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1895
|
+
for (const item of selected) {
|
|
1896
|
+
const id = item.manifest.id;
|
|
1897
|
+
if (byId.has(id)) {
|
|
1898
|
+
addDiagnostic(diagnostics, {
|
|
1899
|
+
code: "COMPOSITION_DUPLICATE_TEMPLATE",
|
|
1900
|
+
severity: "error",
|
|
1901
|
+
message: `Template ID "${id}" is selected more than once.`,
|
|
1902
|
+
templateIds: [id]
|
|
1903
|
+
});
|
|
1904
|
+
} else {
|
|
1905
|
+
byId.set(id, item);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
if (diagnostics.some((d) => d.severity === "error")) {
|
|
1909
|
+
return { diagnostics: Object.freeze(diagnostics) };
|
|
1910
|
+
}
|
|
1911
|
+
const templates = [...byId.values()];
|
|
1912
|
+
const projectTemplates = templates.filter((item) => item.manifest.kind === "project");
|
|
1913
|
+
if (projectTemplates.length > 1) {
|
|
1914
|
+
addDiagnostic(diagnostics, {
|
|
1915
|
+
code: "COMPOSITION_MULTIPLE_PROJECTS",
|
|
1916
|
+
severity: "error",
|
|
1917
|
+
message: "A composition may contain at most one project template.",
|
|
1918
|
+
templateIds: projectTemplates.map((item) => item.manifest.id).sort()
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
const providers = /* @__PURE__ */ new Map();
|
|
1922
|
+
for (const item of templates) {
|
|
1923
|
+
for (const capability of stableUnique(item.manifest.provides ?? [])) {
|
|
1924
|
+
const list = providers.get(capability) ?? [];
|
|
1925
|
+
list.push(item.manifest.id);
|
|
1926
|
+
providers.set(capability, list);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
for (const list of providers.values()) list.sort((a, b) => a.localeCompare(b));
|
|
1930
|
+
const edges = [];
|
|
1931
|
+
for (const item of templates) {
|
|
1932
|
+
const consumer = item.manifest.id;
|
|
1933
|
+
for (const requirement of stableUnique(item.manifest.requires ?? [])) {
|
|
1934
|
+
const matches = providers.get(requirement) ?? [];
|
|
1935
|
+
if (matches.length === 0) {
|
|
1936
|
+
addDiagnostic(diagnostics, {
|
|
1937
|
+
code: "COMPOSITION_MISSING_PROVIDER",
|
|
1938
|
+
severity: "error",
|
|
1939
|
+
message: `Template "${consumer}" requires "${requirement}", but no selected template provides it.`,
|
|
1940
|
+
templateIds: [consumer],
|
|
1941
|
+
requirement
|
|
1942
|
+
});
|
|
1943
|
+
} else if (matches.length > 1) {
|
|
1944
|
+
addDiagnostic(diagnostics, {
|
|
1945
|
+
code: "COMPOSITION_AMBIGUOUS_PROVIDER",
|
|
1946
|
+
severity: "error",
|
|
1947
|
+
message: `Requirement "${requirement}" for "${consumer}" has multiple providers: ${matches.join(", ")}.`,
|
|
1948
|
+
templateIds: [consumer, ...matches],
|
|
1949
|
+
requirement
|
|
1950
|
+
});
|
|
1951
|
+
} else if (matches[0] === consumer) {
|
|
1952
|
+
addDiagnostic(diagnostics, {
|
|
1953
|
+
code: "COMPOSITION_CYCLE",
|
|
1954
|
+
severity: "error",
|
|
1955
|
+
message: `Template "${consumer}" cannot satisfy its own requirement "${requirement}".`,
|
|
1956
|
+
templateIds: [consumer],
|
|
1957
|
+
requirement
|
|
1958
|
+
});
|
|
1959
|
+
} else {
|
|
1960
|
+
edges.push({ from: consumer, to: matches[0], requirement });
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
const selectedIds = new Set(byId.keys());
|
|
1965
|
+
const providedCapabilities = /* @__PURE__ */ new Map();
|
|
1966
|
+
for (const item of templates) {
|
|
1967
|
+
for (const capability of item.manifest.provides ?? []) {
|
|
1968
|
+
const owners = providedCapabilities.get(capability) ?? [];
|
|
1969
|
+
owners.push(item.manifest.id);
|
|
1970
|
+
providedCapabilities.set(capability, owners);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
const reportedConflicts = /* @__PURE__ */ new Set();
|
|
1974
|
+
for (const item of templates) {
|
|
1975
|
+
const id = item.manifest.id;
|
|
1976
|
+
for (const conflict of stableUnique(item.manifest.conflicts ?? [])) {
|
|
1977
|
+
const targets = selectedIds.has(conflict) ? [conflict] : providedCapabilities.get(conflict) ?? [];
|
|
1978
|
+
for (const target of targets) {
|
|
1979
|
+
if (target === id) continue;
|
|
1980
|
+
const pair = [id, target].sort();
|
|
1981
|
+
const pairKey = pair.join("\\0");
|
|
1982
|
+
if (reportedConflicts.has(pairKey)) continue;
|
|
1983
|
+
reportedConflicts.add(pairKey);
|
|
1984
|
+
addDiagnostic(diagnostics, {
|
|
1985
|
+
code: "COMPOSITION_CONFLICT",
|
|
1986
|
+
severity: "error",
|
|
1987
|
+
message: `Template "${id}" conflicts with selected template "${target}" (declaration: "${conflict}").`,
|
|
1988
|
+
templateIds: pair,
|
|
1989
|
+
requirement: conflict
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
if (diagnostics.some((d) => d.severity === "error")) {
|
|
1995
|
+
return { diagnostics: Object.freeze(diagnostics) };
|
|
1996
|
+
}
|
|
1997
|
+
const ids = [...byId.keys()];
|
|
1998
|
+
const ordered = topologicalOrder(ids, edges);
|
|
1999
|
+
if (!ordered) {
|
|
2000
|
+
addDiagnostic(diagnostics, {
|
|
2001
|
+
code: "COMPOSITION_CYCLE",
|
|
2002
|
+
severity: "error",
|
|
2003
|
+
message: "Selected templates contain a dependency cycle.",
|
|
2004
|
+
templateIds: [...ids].sort()
|
|
2005
|
+
});
|
|
2006
|
+
return { diagnostics: Object.freeze(diagnostics) };
|
|
2007
|
+
}
|
|
2008
|
+
const manifestById = /* @__PURE__ */ new Map();
|
|
2009
|
+
for (const [id, item] of byId) manifestById.set(id, item.manifest);
|
|
2010
|
+
const plan = Object.freeze({
|
|
2011
|
+
orderedTemplateIds: Object.freeze(ordered),
|
|
2012
|
+
templates: Object.freeze(ordered.map((id) => manifestById.get(id))),
|
|
2013
|
+
edges: Object.freeze(edges.sort(
|
|
2014
|
+
(a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.requirement.localeCompare(b.requirement)
|
|
2015
|
+
))
|
|
2016
|
+
});
|
|
2017
|
+
return { plan, diagnostics: Object.freeze(diagnostics) };
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// ../sdk/src/execution/plan-file-operations.ts
|
|
2021
|
+
function cloneContent(content) {
|
|
2022
|
+
return typeof content === "string" ? content : Buffer.from(content);
|
|
2023
|
+
}
|
|
2024
|
+
function planFileOperations(templates, mode = "strict") {
|
|
2025
|
+
const composition = resolveComposition(templates.map((template) => ({
|
|
2026
|
+
manifest: template.manifest,
|
|
2027
|
+
source: template.root
|
|
2028
|
+
})));
|
|
2029
|
+
const diagnostics = composition.diagnostics.map((diagnostic3) => ({
|
|
2030
|
+
code: diagnostic3.code,
|
|
2031
|
+
severity: "error",
|
|
2032
|
+
message: diagnostic3.message,
|
|
2033
|
+
templateIds: diagnostic3.templateIds
|
|
2034
|
+
}));
|
|
2035
|
+
if (!composition.plan) {
|
|
2036
|
+
return { diagnostics: Object.freeze(diagnostics) };
|
|
2037
|
+
}
|
|
2038
|
+
const byId = new Map(templates.map((template) => [template.manifest.id, template]));
|
|
2039
|
+
const operations = [];
|
|
2040
|
+
const operationByPath = /* @__PURE__ */ new Map();
|
|
2041
|
+
for (const templateId of composition.plan.orderedTemplateIds) {
|
|
2042
|
+
const template = byId.get(templateId);
|
|
2043
|
+
for (const file of template.vfs.list()) {
|
|
2044
|
+
let destination;
|
|
2045
|
+
try {
|
|
2046
|
+
destination = normalizeVfsDestination(file.dest);
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
diagnostics.push({
|
|
2049
|
+
code: "FILE_PATH_INVALID",
|
|
2050
|
+
severity: "error",
|
|
2051
|
+
message: `Template "${templateId}" contains an invalid output path "${file.dest}": ${error instanceof Error ? error.message : String(error)}`,
|
|
2052
|
+
destination: file.dest,
|
|
2053
|
+
templateIds: [templateId]
|
|
2054
|
+
});
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
const existingIndex = operationByPath.get(destination);
|
|
2058
|
+
if (existingIndex === void 0) {
|
|
2059
|
+
operationByPath.set(destination, operations.length);
|
|
2060
|
+
operations.push(Object.freeze({
|
|
2061
|
+
kind: "create",
|
|
2062
|
+
destination,
|
|
2063
|
+
sourceTemplateId: templateId,
|
|
2064
|
+
content: cloneContent(file.content),
|
|
2065
|
+
render: file.render,
|
|
2066
|
+
binary: file.binary,
|
|
2067
|
+
replaces: Object.freeze([])
|
|
2068
|
+
}));
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
const previous = operations[existingIndex];
|
|
2072
|
+
const involved = [previous.sourceTemplateId, templateId];
|
|
2073
|
+
if (mode === "strict") {
|
|
2074
|
+
diagnostics.push({
|
|
2075
|
+
code: "FILE_CONFLICT",
|
|
2076
|
+
severity: "error",
|
|
2077
|
+
message: `Multiple templates produce "${destination}": ${involved.join(", ")}.`,
|
|
2078
|
+
destination,
|
|
2079
|
+
templateIds: Object.freeze(involved)
|
|
2080
|
+
});
|
|
2081
|
+
continue;
|
|
2082
|
+
}
|
|
2083
|
+
diagnostics.push({
|
|
2084
|
+
code: "FILE_CONFLICT",
|
|
2085
|
+
severity: "warning",
|
|
2086
|
+
message: `Template "${templateId}" overlays "${destination}" from "${previous.sourceTemplateId}".`,
|
|
2087
|
+
destination,
|
|
2088
|
+
templateIds: Object.freeze(involved)
|
|
2089
|
+
});
|
|
2090
|
+
operations[existingIndex] = Object.freeze({
|
|
2091
|
+
kind: "overwrite",
|
|
2092
|
+
destination,
|
|
2093
|
+
sourceTemplateId: templateId,
|
|
2094
|
+
content: cloneContent(file.content),
|
|
2095
|
+
render: file.render,
|
|
2096
|
+
binary: file.binary,
|
|
2097
|
+
replaces: Object.freeze([...previous.replaces, previous.sourceTemplateId])
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
if (diagnostics.some((diagnostic3) => diagnostic3.severity === "error")) {
|
|
2102
|
+
return { diagnostics: Object.freeze(diagnostics) };
|
|
2103
|
+
}
|
|
2104
|
+
const orderedTemplates = composition.plan.orderedTemplateIds.map((id) => byId.get(id));
|
|
2105
|
+
const plan = Object.freeze({
|
|
2106
|
+
mode,
|
|
2107
|
+
orderedTemplateIds: Object.freeze([...composition.plan.orderedTemplateIds]),
|
|
2108
|
+
operations: Object.freeze(operations),
|
|
2109
|
+
templates: Object.freeze(orderedTemplates)
|
|
2110
|
+
});
|
|
2111
|
+
return { plan, diagnostics: Object.freeze(diagnostics) };
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// ../sdk/src/execution/execute-file-plan.ts
|
|
2115
|
+
async function executeFilePlan(plan, targetDirectory, options = {}) {
|
|
2116
|
+
return executeFileOperations(
|
|
2117
|
+
targetDirectory,
|
|
2118
|
+
plan.operations.map((operation) => ({
|
|
2119
|
+
destination: operation.destination,
|
|
2120
|
+
content: operation.content,
|
|
2121
|
+
binary: operation.binary,
|
|
2122
|
+
sourceTemplateId: operation.sourceTemplateId
|
|
2123
|
+
})),
|
|
2124
|
+
options
|
|
2125
|
+
);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// ../sdk/src/execution/render-file-operation-plan.ts
|
|
2129
|
+
async function renderFileOperationPlan(plan, variablesByTemplate = /* @__PURE__ */ new Map(), schemasByTemplate = /* @__PURE__ */ new Map()) {
|
|
2130
|
+
const diagnostics = [];
|
|
2131
|
+
const templates = new Map(plan.templates.map((template) => [template.manifest.id, template]));
|
|
2132
|
+
const validatedVariables = /* @__PURE__ */ new Map();
|
|
2133
|
+
for (const template of plan.templates) {
|
|
2134
|
+
const id = template.manifest.id;
|
|
2135
|
+
const variables = variablesByTemplate.get(id) ?? {};
|
|
2136
|
+
const validation = validateTemplateVariables(variables, schemasByTemplate.get(id));
|
|
2137
|
+
if (!validation.valid) {
|
|
2138
|
+
for (const message of validation.errors) {
|
|
2139
|
+
diagnostics.push({ code: "RENDER_INVALID_VARIABLES", templateId: id, message });
|
|
2140
|
+
}
|
|
2141
|
+
} else {
|
|
2142
|
+
validatedVariables.set(id, variables);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (diagnostics.length > 0) {
|
|
2146
|
+
return Object.freeze({ ok: false, diagnostics: Object.freeze(diagnostics) });
|
|
2147
|
+
}
|
|
2148
|
+
const operations = [];
|
|
2149
|
+
for (const operation of plan.operations) {
|
|
2150
|
+
const template = templates.get(operation.sourceTemplateId);
|
|
2151
|
+
if (!template) {
|
|
2152
|
+
diagnostics.push({
|
|
2153
|
+
code: "RENDER_FILE_FAILED",
|
|
2154
|
+
templateId: operation.sourceTemplateId,
|
|
2155
|
+
destination: operation.destination,
|
|
2156
|
+
message: "File operation refers to a template that is not in the composition plan."
|
|
2157
|
+
});
|
|
2158
|
+
continue;
|
|
2159
|
+
}
|
|
2160
|
+
try {
|
|
2161
|
+
const rendered = await renderFile({
|
|
2162
|
+
file: {
|
|
2163
|
+
dest: operation.destination,
|
|
2164
|
+
content: operation.content,
|
|
2165
|
+
render: operation.render,
|
|
2166
|
+
binary: operation.binary
|
|
2167
|
+
},
|
|
2168
|
+
engine: template.manifest.renderer.engine,
|
|
2169
|
+
variables: validatedVariables.get(operation.sourceTemplateId) ?? {}
|
|
2170
|
+
});
|
|
2171
|
+
operations.push(Object.freeze({ ...operation, content: rendered.content, render: false }));
|
|
2172
|
+
} catch (error) {
|
|
2173
|
+
diagnostics.push({
|
|
2174
|
+
code: "RENDER_FILE_FAILED",
|
|
2175
|
+
templateId: operation.sourceTemplateId,
|
|
2176
|
+
destination: operation.destination,
|
|
2177
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
if (diagnostics.length > 0) {
|
|
2182
|
+
return Object.freeze({ ok: false, diagnostics: Object.freeze(diagnostics) });
|
|
2183
|
+
}
|
|
2184
|
+
return Object.freeze({
|
|
2185
|
+
ok: true,
|
|
2186
|
+
plan: Object.freeze({
|
|
2187
|
+
sourcePlan: plan,
|
|
2188
|
+
operations: Object.freeze(operations)
|
|
2189
|
+
}),
|
|
2190
|
+
diagnostics: Object.freeze([])
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// src/template-path.ts
|
|
2195
|
+
import { existsSync } from "fs";
|
|
2196
|
+
import path15 from "path";
|
|
2197
|
+
import { fileURLToPath } from "url";
|
|
2198
|
+
function resolveTemplateDirectory(override) {
|
|
2199
|
+
if (override) return path15.resolve(override);
|
|
2200
|
+
if (process.env.APPINIT_TEMPLATES_DIR) {
|
|
2201
|
+
return path15.resolve(process.env.APPINIT_TEMPLATES_DIR);
|
|
2202
|
+
}
|
|
2203
|
+
const workingDirectoryTemplates = path15.resolve(process.cwd(), "templates");
|
|
2204
|
+
if (existsSync(workingDirectoryTemplates)) return workingDirectoryTemplates;
|
|
2205
|
+
return path15.join(path15.dirname(fileURLToPath(import.meta.url)), "templates");
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// src/create.ts
|
|
2209
|
+
async function discoverApprovedRoots(directory) {
|
|
2210
|
+
const canonicalRoot = await realpath7(path16.resolve(directory));
|
|
2211
|
+
const entries = await readdir4(canonicalRoot, { withFileTypes: true });
|
|
2212
|
+
const roots = /* @__PURE__ */ new Map();
|
|
2213
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2214
|
+
if (!entry.isDirectory()) continue;
|
|
2215
|
+
const templateRoot = path16.join(canonicalRoot, entry.name);
|
|
2216
|
+
let manifestValue;
|
|
2217
|
+
try {
|
|
2218
|
+
manifestValue = JSON.parse(await readFile9(path16.join(templateRoot, "appinit.template.json"), "utf8"));
|
|
2219
|
+
} catch {
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
if (manifestValue === null || typeof manifestValue !== "object" || Array.isArray(manifestValue)) continue;
|
|
2223
|
+
const manifest = manifestValue;
|
|
2224
|
+
const aliases = [entry.name, manifest.id, manifest.packageName].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
2225
|
+
for (const alias of aliases) {
|
|
2226
|
+
const existing = roots.get(alias);
|
|
2227
|
+
if (existing && existing !== templateRoot) throw new Error(`Template reference "${alias}" is ambiguous in ${canonicalRoot}.`);
|
|
2228
|
+
roots.set(alias, templateRoot);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
return roots;
|
|
2232
|
+
}
|
|
2233
|
+
async function discoverLocalTemplates(directory) {
|
|
2234
|
+
const roots = await discoverApprovedRoots(directory);
|
|
2235
|
+
const uniqueRoots = [...new Set(roots.values())].sort();
|
|
2236
|
+
const templates = [];
|
|
2237
|
+
for (const root of uniqueRoots) {
|
|
2238
|
+
const manifest = JSON.parse(await readFile9(path16.join(root, "appinit.template.json"), "utf8"));
|
|
2239
|
+
if (typeof manifest.id !== "string") continue;
|
|
2240
|
+
try {
|
|
2241
|
+
const discovered = await discoverTemplate(manifest.id, { templateRoots: roots });
|
|
2242
|
+
templates.push(Object.freeze({
|
|
2243
|
+
id: discovered.template.manifest.id,
|
|
2244
|
+
name: discovered.template.manifest.name,
|
|
2245
|
+
description: discovered.template.manifest.description,
|
|
2246
|
+
category: discovered.template.manifest.category,
|
|
2247
|
+
root,
|
|
2248
|
+
template: discovered.template
|
|
2249
|
+
}));
|
|
2250
|
+
} catch {
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
return Object.freeze(templates);
|
|
2254
|
+
}
|
|
2255
|
+
async function assertSafeTarget(target) {
|
|
2256
|
+
const absolute = path16.resolve(target);
|
|
2257
|
+
try {
|
|
2258
|
+
const info = await lstat6(absolute);
|
|
2259
|
+
if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("Target must be a real directory, not a symlink or file.");
|
|
2260
|
+
if ((await readdir4(absolute)).length > 0) throw new Error(`Target directory is not empty: ${absolute}. Choose an empty or new directory.`);
|
|
2261
|
+
} catch (error) {
|
|
2262
|
+
if (error.code !== "ENOENT") throw error;
|
|
2263
|
+
}
|
|
2264
|
+
return absolute;
|
|
2265
|
+
}
|
|
2266
|
+
async function recordProjectHistory(targetDirectory, templateId) {
|
|
2267
|
+
const homeDirectory = resolveAppInitHome();
|
|
2268
|
+
const existing = await readAppInitJson(homeDirectory, "state.json");
|
|
2269
|
+
let state;
|
|
2270
|
+
if (existing === void 0) {
|
|
2271
|
+
state = { schemaVersion: 1, projects: [] };
|
|
2272
|
+
} else {
|
|
2273
|
+
const validation = validateAppInitState(existing);
|
|
2274
|
+
if (!validation.valid || !validation.state) {
|
|
2275
|
+
throw new Error(`Existing global state is invalid: ${validation.errors.join(" ")}`);
|
|
2276
|
+
}
|
|
2277
|
+
state = validation.state;
|
|
2278
|
+
}
|
|
2279
|
+
const projectPath = path16.resolve(targetDirectory);
|
|
2280
|
+
const projects = state.projects.filter((project) => project.path !== projectPath);
|
|
2281
|
+
projects.push({
|
|
2282
|
+
path: projectPath,
|
|
2283
|
+
templateId,
|
|
2284
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2285
|
+
});
|
|
2286
|
+
await writeAppInitJson(homeDirectory, "state.json", {
|
|
2287
|
+
...state,
|
|
2288
|
+
schemaVersion: 1,
|
|
2289
|
+
projects
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
async function runCreateCommand(options) {
|
|
2293
|
+
const targetDirectory = await assertSafeTarget(options.target);
|
|
2294
|
+
const templateDirectory = resolveTemplateDirectory(options.templateDirectory);
|
|
2295
|
+
const intent = {
|
|
2296
|
+
executionKind: "scaffold",
|
|
2297
|
+
executionMode: "user",
|
|
2298
|
+
artifactKind: "project",
|
|
2299
|
+
rootDir: process.cwd(),
|
|
2300
|
+
targetDir: targetDirectory,
|
|
2301
|
+
templateRef: options.template,
|
|
2302
|
+
variables: { ...options.variables },
|
|
2303
|
+
flags: { dryRun: options.dryRun }
|
|
2304
|
+
};
|
|
2305
|
+
const validation = validateExecutionIntent(intent);
|
|
2306
|
+
if (!validation.ok) throw new Error(validation.diagnostics.map((item) => item.message).join("\n"));
|
|
2307
|
+
const roots = await discoverApprovedRoots(templateDirectory);
|
|
2308
|
+
const approvedRoot = roots.get(options.template);
|
|
2309
|
+
if (!approvedRoot) throw new Error(`Template "${options.template}" is not present in the approved template roots.`);
|
|
2310
|
+
const approvedManifest = JSON.parse(await readFile9(path16.join(approvedRoot, "appinit.template.json"), "utf8"));
|
|
2311
|
+
if (typeof approvedManifest.id !== "string") throw new Error(`Template "${options.template}" has an invalid manifest identity.`);
|
|
2312
|
+
const discovered = await discoverTemplate(approvedManifest.id, { templateRoots: roots });
|
|
2313
|
+
const metadata = await loadTemplateVariables(discovered.template.root, discovered.template.manifest);
|
|
2314
|
+
const variables = { ...metadata.defaults, ...options.variables };
|
|
2315
|
+
const variableErrors = validateTemplateVariables2(variables, metadata.schema);
|
|
2316
|
+
if (variableErrors.length) throw new Error(`Template variables are invalid:
|
|
2317
|
+
${variableErrors.map((item) => ` - ${item}`).join("\n")}`);
|
|
2318
|
+
const planning = planFileOperations([discovered.template], "strict");
|
|
2319
|
+
if (!planning.plan) throw new Error(`Composition planning failed:
|
|
2320
|
+
${planning.diagnostics.map((item) => item.message).join("\n")}`);
|
|
2321
|
+
const schema = {};
|
|
2322
|
+
for (const [key, rule] of Object.entries(metadata.schema)) {
|
|
2323
|
+
if (rule.type) schema[key] = { type: rule.type, ...rule.required !== void 0 ? { required: rule.required } : {}, ...rule.minLength !== void 0 ? { minLength: rule.minLength } : {}, ...rule.enum ? { enum: rule.enum } : {} };
|
|
2324
|
+
}
|
|
2325
|
+
const templateId = discovered.template.manifest.id;
|
|
2326
|
+
const rendered = await renderFileOperationPlan(
|
|
2327
|
+
planning.plan,
|
|
2328
|
+
/* @__PURE__ */ new Map([[templateId, variables]]),
|
|
2329
|
+
/* @__PURE__ */ new Map([[templateId, schema]])
|
|
2330
|
+
);
|
|
2331
|
+
if (!rendered.ok || !rendered.plan) throw new Error(`Template rendering failed:
|
|
2332
|
+
${rendered.diagnostics.map((item) => item.message).join("\n")}`);
|
|
2333
|
+
const executablePlan = { ...rendered.plan.sourcePlan, operations: rendered.plan.operations };
|
|
2334
|
+
const execution = await executeFilePlan(executablePlan, targetDirectory, { dryRun: options.dryRun });
|
|
2335
|
+
if (!execution.ok) throw new Error(`File execution failed:
|
|
2336
|
+
${execution.diagnostics.map((item) => item.message).join("\n")}`);
|
|
2337
|
+
if (options.dryRun) {
|
|
2338
|
+
process.stdout.write(`Dry run successful. ${execution.writtenFiles.length} file(s) would be created in ${targetDirectory}.
|
|
2339
|
+
`);
|
|
2340
|
+
for (const destination of execution.writtenFiles) process.stdout.write(` + ${destination}
|
|
2341
|
+
`);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
const operations = rendered.plan.operations.map((operation) => ({
|
|
2345
|
+
destination: operation.destination,
|
|
2346
|
+
content: operation.content,
|
|
2347
|
+
binary: operation.binary,
|
|
2348
|
+
sourceTemplateId: operation.sourceTemplateId
|
|
2349
|
+
}));
|
|
2350
|
+
const verification = await verifyGeneratedOutput(targetDirectory, operations);
|
|
2351
|
+
if (!verification.ok) throw new Error(`Output was written, but verification failed. Review ${targetDirectory}:
|
|
2352
|
+
${verification.diagnostics.map((item) => item.message).join("\n")}`);
|
|
2353
|
+
try {
|
|
2354
|
+
await recordProjectHistory(targetDirectory, templateId);
|
|
2355
|
+
} catch (error) {
|
|
2356
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2357
|
+
process.stderr.write(`Warning: project was created, but global history could not be updated: ${message}
|
|
2358
|
+
`);
|
|
2359
|
+
}
|
|
2360
|
+
process.stdout.write(`Created ${discovered.template.manifest.name} in ${targetDirectory}.
|
|
2361
|
+
`);
|
|
2362
|
+
process.stdout.write(`Verified ${verification.actualFiles.length} generated file(s).
|
|
2363
|
+
`);
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// src/finalize-project.ts
|
|
2367
|
+
import { access as access2, readFile as readFile11 } from "fs/promises";
|
|
2368
|
+
import path18 from "path";
|
|
2369
|
+
|
|
2370
|
+
// src/finalize.ts
|
|
2371
|
+
import { access, readFile as readFile10 } from "fs/promises";
|
|
2372
|
+
import { spawn } from "child_process";
|
|
2373
|
+
import path17 from "path";
|
|
2374
|
+
var LOCKFILES = Object.freeze({
|
|
2375
|
+
"pnpm-lock.yaml": "pnpm",
|
|
2376
|
+
"package-lock.json": "npm",
|
|
2377
|
+
"yarn.lock": "yarn",
|
|
2378
|
+
"bun.lock": "bun",
|
|
2379
|
+
"bun.lockb": "bun"
|
|
2380
|
+
});
|
|
2381
|
+
async function detectPackageManager(directory) {
|
|
2382
|
+
try {
|
|
2383
|
+
const manifest = JSON.parse(await readFile10(path17.join(directory, "package.json"), "utf8"));
|
|
2384
|
+
if (typeof manifest.packageManager === "string") {
|
|
2385
|
+
const declared = /^(pnpm|npm|yarn|bun)@/.exec(manifest.packageManager)?.[1];
|
|
2386
|
+
if (declared) return declared;
|
|
2387
|
+
}
|
|
2388
|
+
} catch {
|
|
2389
|
+
}
|
|
2390
|
+
const detected = /* @__PURE__ */ new Set();
|
|
2391
|
+
for (const [lockfile, manager] of Object.entries(LOCKFILES)) {
|
|
2392
|
+
try {
|
|
2393
|
+
await access(path17.join(directory, lockfile));
|
|
2394
|
+
detected.add(manager);
|
|
2395
|
+
} catch {
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
if (detected.size === 1) return detected.values().next().value;
|
|
2399
|
+
if (detected.size > 1) return void 0;
|
|
2400
|
+
const agent = process.env.npm_config_user_agent ?? "";
|
|
2401
|
+
const match = /^(pnpm|npm|yarn|bun)\//.exec(agent);
|
|
2402
|
+
return match?.[1];
|
|
2403
|
+
}
|
|
2404
|
+
async function isNestedInPnpmWorkspace(directory) {
|
|
2405
|
+
let current = path17.dirname(path17.resolve(directory));
|
|
2406
|
+
const root = path17.parse(current).root;
|
|
2407
|
+
while (true) {
|
|
2408
|
+
try {
|
|
2409
|
+
await access(path17.join(current, "pnpm-workspace.yaml"));
|
|
2410
|
+
return true;
|
|
2411
|
+
} catch {
|
|
2412
|
+
if (current === root) return false;
|
|
2413
|
+
current = path17.dirname(current);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
async function runSetupCommand(command, directory, packageManager) {
|
|
2418
|
+
const executable = command === "git-init" ? "git" : packageManager;
|
|
2419
|
+
if (!executable) throw new Error("A package manager must be selected before installing dependencies.");
|
|
2420
|
+
const args = command === "git-init" ? ["init"] : ["install"];
|
|
2421
|
+
if (command === "install" && packageManager === "pnpm" && await isNestedInPnpmWorkspace(directory)) {
|
|
2422
|
+
args.push("--ignore-workspace");
|
|
2423
|
+
}
|
|
2424
|
+
await new Promise((resolve, reject) => {
|
|
2425
|
+
const child = spawn(executable, args, {
|
|
2426
|
+
cwd: directory,
|
|
2427
|
+
stdio: "inherit",
|
|
2428
|
+
shell: process.platform === "win32"
|
|
2429
|
+
});
|
|
2430
|
+
child.once("error", (error) => reject(new Error(`Could not start ${executable}: ${error.message}`)));
|
|
2431
|
+
child.once("close", (code, signal) => {
|
|
2432
|
+
if (code === 0) resolve();
|
|
2433
|
+
else reject(new Error(`${executable} ${args.join(" ")} failed${signal ? ` (signal ${signal})` : ` with exit code ${String(code)}`}.`));
|
|
2434
|
+
});
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// src/finalize-project.ts
|
|
2439
|
+
async function isInsideGitRepository(directory) {
|
|
2440
|
+
let current = path18.resolve(directory);
|
|
2441
|
+
const root = path18.parse(current).root;
|
|
2442
|
+
while (true) {
|
|
2443
|
+
try {
|
|
2444
|
+
await access2(path18.join(current, ".git"));
|
|
2445
|
+
return true;
|
|
2446
|
+
} catch {
|
|
2447
|
+
if (current === root) return false;
|
|
2448
|
+
current = path18.dirname(current);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
async function getDevScript(directory) {
|
|
2453
|
+
try {
|
|
2454
|
+
const manifest = JSON.parse(await readFile11(path18.join(directory, "package.json"), "utf8"));
|
|
2455
|
+
return typeof manifest.scripts?.dev === "string" ? manifest.scripts.dev : void 0;
|
|
2456
|
+
} catch {
|
|
2457
|
+
return void 0;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
function isPackageManager(value) {
|
|
2461
|
+
return value === "pnpm" || value === "npm" || value === "yarn" || value === "bun";
|
|
2462
|
+
}
|
|
2463
|
+
async function finalizeProject(directory, options, io) {
|
|
2464
|
+
let install = options.install;
|
|
2465
|
+
let git = options.git;
|
|
2466
|
+
const failures = [];
|
|
2467
|
+
if (io.interactive) {
|
|
2468
|
+
if (!install) install = await io.confirm("Install project dependencies now?");
|
|
2469
|
+
if (!git) git = await io.confirm("Initialize a Git repository?");
|
|
2470
|
+
}
|
|
2471
|
+
let manager = options.packageManager ?? await detectPackageManager(directory);
|
|
2472
|
+
if (install && !manager && io.interactive) {
|
|
2473
|
+
const answer = (await io.ask("Package manager (pnpm/npm/yarn/bun)", "pnpm")).toLowerCase();
|
|
2474
|
+
if (!isPackageManager(answer)) throw new Error(`Unsupported package manager "${answer}". Choose pnpm, npm, yarn, or bun.`);
|
|
2475
|
+
manager = answer;
|
|
2476
|
+
}
|
|
2477
|
+
if (install) {
|
|
2478
|
+
if (!manager) {
|
|
2479
|
+
const message = "No package manager detected. Re-run with --package-manager <pnpm|npm|yarn|bun> --install.";
|
|
2480
|
+
failures.push(message);
|
|
2481
|
+
io.write(`Dependencies were not installed: ${message}
|
|
2482
|
+
`);
|
|
2483
|
+
} else {
|
|
2484
|
+
io.write(`Installing dependencies with ${manager}...
|
|
2485
|
+
`);
|
|
2486
|
+
try {
|
|
2487
|
+
await runSetupCommand("install", directory, manager);
|
|
2488
|
+
io.write("Dependencies installed successfully.\n");
|
|
2489
|
+
} catch (error) {
|
|
2490
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2491
|
+
failures.push(message);
|
|
2492
|
+
io.write(`Dependency installation failed; generated files were preserved. ${message}
|
|
2493
|
+
`);
|
|
2494
|
+
io.write(`Recovery: cd "${directory}" && ${manager} install
|
|
2495
|
+
`);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
if (git) {
|
|
2500
|
+
if (await isInsideGitRepository(directory)) {
|
|
2501
|
+
io.write("Skipped Git initialization: this project is already inside a Git repository.\n");
|
|
2502
|
+
} else {
|
|
2503
|
+
try {
|
|
2504
|
+
await runSetupCommand("git-init", directory);
|
|
2505
|
+
io.write("Git repository initialized.\n");
|
|
2506
|
+
} catch (error) {
|
|
2507
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2508
|
+
failures.push(message);
|
|
2509
|
+
io.write(`Git initialization failed; generated files were preserved. ${message}
|
|
2510
|
+
`);
|
|
2511
|
+
io.write(`Recovery: cd "${directory}" && git init
|
|
2512
|
+
`);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
io.write("\nNext steps:\n");
|
|
2517
|
+
io.write(` cd "${directory}"
|
|
2518
|
+
`);
|
|
2519
|
+
if (install && manager && failures.length === 0) {
|
|
2520
|
+
const devScript = await getDevScript(directory);
|
|
2521
|
+
if (devScript) {
|
|
2522
|
+
const run = manager === "npm" ? "npm run dev" : manager === "bun" ? "bun run dev" : `${manager} dev`;
|
|
2523
|
+
io.write(` ${run}
|
|
2524
|
+
`);
|
|
2525
|
+
} else {
|
|
2526
|
+
io.write(" Review the generated README for available scripts.\n");
|
|
2527
|
+
}
|
|
2528
|
+
} else {
|
|
2529
|
+
io.write(` ${manager ? manager + " install" : "Install dependencies with your package manager"}
|
|
2530
|
+
`);
|
|
2531
|
+
}
|
|
2532
|
+
if (failures.length) {
|
|
2533
|
+
throw new Error(`Project files were created, but one or more setup steps failed: ${failures.join("; ")}`);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// src/doctor.ts
|
|
2538
|
+
import path22 from "path";
|
|
2539
|
+
|
|
2540
|
+
// ../guard/src/doctor/providers.ts
|
|
2541
|
+
import { readdir as readdir5, readFile as readFile12 } from "fs/promises";
|
|
2542
|
+
import path19 from "path";
|
|
2543
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules", ".pnpm", "dist"]);
|
|
2544
|
+
async function walk(root, current, files) {
|
|
2545
|
+
let entries;
|
|
2546
|
+
try {
|
|
2547
|
+
entries = await readdir5(current, { withFileTypes: true });
|
|
2548
|
+
} catch {
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
for (const entry of entries) {
|
|
2552
|
+
if (entry.isSymbolicLink()) continue;
|
|
2553
|
+
const absolute = path19.join(current, entry.name);
|
|
2554
|
+
const relative = path19.relative(root, absolute).split(path19.sep).join("/");
|
|
2555
|
+
files.add(relative);
|
|
2556
|
+
if (entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name)) {
|
|
2557
|
+
await walk(root, absolute, files);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
async function readJson(filePath) {
|
|
2562
|
+
try {
|
|
2563
|
+
const value = JSON.parse(await readFile12(filePath, "utf8"));
|
|
2564
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2565
|
+
} catch {
|
|
2566
|
+
return void 0;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
async function collectDoctorSnapshot(rootDir) {
|
|
2570
|
+
const root = path19.resolve(rootDir);
|
|
2571
|
+
const files = /* @__PURE__ */ new Set();
|
|
2572
|
+
await walk(root, root, files);
|
|
2573
|
+
const packageJson = await readJson(path19.join(root, "package.json"));
|
|
2574
|
+
const appinitContext = await readJson(path19.join(root, ".appinit", "context.json"));
|
|
2575
|
+
const declaration = packageJson?.packageManager;
|
|
2576
|
+
const declaredManager = typeof declaration === "string" ? /^(pnpm|npm|yarn|bun)@/.exec(declaration)?.[1] : void 0;
|
|
2577
|
+
const lockfileManager = files.has("pnpm-lock.yaml") ? "pnpm" : files.has("package-lock.json") ? "npm" : files.has("yarn.lock") ? "yarn" : files.has("bun.lock") || files.has("bun.lockb") ? "bun" : void 0;
|
|
2578
|
+
const intent = {
|
|
2579
|
+
artifactKind: appinitContext?.artifactKind === "template" || appinitContext?.artifactKind === "package" ? appinitContext.artifactKind : "project",
|
|
2580
|
+
atsVersion: typeof appinitContext?.atsVersion === "string" ? appinitContext.atsVersion : void 0,
|
|
2581
|
+
runtime: appinitContext?.runtime === "node" || appinitContext?.runtime === "bun" || appinitContext?.runtime === "deno" ? appinitContext.runtime : void 0,
|
|
2582
|
+
framework: typeof appinitContext?.framework === "string" ? appinitContext.framework : void 0
|
|
2583
|
+
};
|
|
2584
|
+
const dependencies = {
|
|
2585
|
+
...packageJson?.dependencies && typeof packageJson.dependencies === "object" ? packageJson.dependencies : {},
|
|
2586
|
+
...packageJson?.devDependencies && typeof packageJson.devDependencies === "object" ? packageJson.devDependencies : {}
|
|
2587
|
+
};
|
|
2588
|
+
return Object.freeze({
|
|
2589
|
+
files,
|
|
2590
|
+
dependencies: Object.freeze(dependencies),
|
|
2591
|
+
packageManager: lockfileManager ?? declaredManager,
|
|
2592
|
+
intent: Object.freeze(intent)
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
// ../guard/src/doctor/rules.ts
|
|
2597
|
+
function diagnostic(code, level, subsystem, message, hint) {
|
|
2598
|
+
return { code, level, subsystem, message, hint, ruleId: code };
|
|
2599
|
+
}
|
|
2600
|
+
var builtInDoctorRules = Object.freeze([
|
|
2601
|
+
{
|
|
2602
|
+
id: "project.package-manager-lock",
|
|
2603
|
+
run: ({ snapshot }) => {
|
|
2604
|
+
const lockfiles = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"].filter((file) => snapshot.files.has(file));
|
|
2605
|
+
return lockfiles.length > 1 ? [
|
|
2606
|
+
diagnostic(
|
|
2607
|
+
"DOCTOR_MULTIPLE_LOCKFILES",
|
|
2608
|
+
"warn",
|
|
2609
|
+
"compatibility",
|
|
2610
|
+
`Multiple package-manager lockfiles were found: ${lockfiles.join(", ")}.`,
|
|
2611
|
+
"Keep the lockfile for the package manager used by this project."
|
|
2612
|
+
)
|
|
2613
|
+
] : [];
|
|
2614
|
+
}
|
|
2615
|
+
},
|
|
2616
|
+
{
|
|
2617
|
+
id: "project.runtime-config",
|
|
2618
|
+
run: ({ snapshot }) => {
|
|
2619
|
+
const runtime = snapshot.intent.runtime;
|
|
2620
|
+
if (runtime === "node" && !snapshot.files.has("package.json")) {
|
|
2621
|
+
return [diagnostic(
|
|
2622
|
+
"DOCTOR_NODE_PACKAGE_JSON_MISSING",
|
|
2623
|
+
"warn",
|
|
2624
|
+
"runtime",
|
|
2625
|
+
"Node.js runtime is declared, but no package.json was found.",
|
|
2626
|
+
"Add a package.json or update .appinit/context.json."
|
|
2627
|
+
)];
|
|
2628
|
+
}
|
|
2629
|
+
if (runtime === "deno" && !snapshot.files.has("deno.json") && !snapshot.files.has("deno.jsonc")) {
|
|
2630
|
+
return [diagnostic(
|
|
2631
|
+
"DOCTOR_DENO_CONFIG_MISSING",
|
|
2632
|
+
"warn",
|
|
2633
|
+
"runtime",
|
|
2634
|
+
"Deno runtime is declared, but no deno.json or deno.jsonc was found.",
|
|
2635
|
+
"Add a Deno configuration file or update .appinit/context.json."
|
|
2636
|
+
)];
|
|
2637
|
+
}
|
|
2638
|
+
if (runtime === "bun" && !snapshot.files.has("bun.lock") && !snapshot.files.has("bun.lockb")) {
|
|
2639
|
+
return [diagnostic(
|
|
2640
|
+
"DOCTOR_BUN_LOCK_MISSING",
|
|
2641
|
+
"info",
|
|
2642
|
+
"runtime",
|
|
2643
|
+
"Bun runtime is declared, but no Bun lockfile was found.",
|
|
2644
|
+
"Generate a Bun lockfile if this project uses Bun for dependency management."
|
|
2645
|
+
)];
|
|
2646
|
+
}
|
|
2647
|
+
return [];
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
]);
|
|
2651
|
+
function runDoctorRules(context, rules = builtInDoctorRules) {
|
|
2652
|
+
const results = [];
|
|
2653
|
+
for (const rule of rules) {
|
|
2654
|
+
try {
|
|
2655
|
+
results.push(...rule.run(context));
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
results.push(diagnostic(
|
|
2658
|
+
"DOCTOR_RULE_FAILED",
|
|
2659
|
+
"warn",
|
|
2660
|
+
"doctor",
|
|
2661
|
+
`Doctor rule "${rule.id}" could not complete: ${error instanceof Error ? error.message : String(error)}`
|
|
2662
|
+
));
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
return results;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// ../guard/src/doctor/doctor.ts
|
|
2669
|
+
import path20 from "path";
|
|
2670
|
+
var Doctor = class {
|
|
2671
|
+
constructor(options = {}) {
|
|
2672
|
+
this.options = options;
|
|
2673
|
+
}
|
|
2674
|
+
async diagnose() {
|
|
2675
|
+
const rootDir = path20.resolve(this.options.rootDir ?? process.cwd());
|
|
2676
|
+
const snapshot = await collectDoctorSnapshot(rootDir);
|
|
2677
|
+
const diagnostics = runDoctorRules({ rootDir, snapshot }, this.options.rules);
|
|
2678
|
+
const summary = { info: 0, warn: 0, error: 0, fatal: 0 };
|
|
2679
|
+
for (const item of diagnostics) summary[item.level] += 1;
|
|
2680
|
+
return Object.freeze({
|
|
2681
|
+
rootDir,
|
|
2682
|
+
diagnostics: Object.freeze([...diagnostics]),
|
|
2683
|
+
summary: Object.freeze(summary),
|
|
2684
|
+
ok: diagnostics.every((item) => item.level !== "error" && item.level !== "fatal")
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
|
|
2689
|
+
// ../guard/src/validation/validator.ts
|
|
2690
|
+
import path21 from "path";
|
|
2691
|
+
function createReport(rootDir, diagnostics) {
|
|
2692
|
+
return Object.freeze({
|
|
2693
|
+
rootDir,
|
|
2694
|
+
diagnostics: Object.freeze([...diagnostics]),
|
|
2695
|
+
hasErrors: diagnostics.some((item) => item.level === "error" || item.level === "fatal"),
|
|
2696
|
+
hasWarnings: diagnostics.some((item) => item.level === "warn"),
|
|
2697
|
+
hasAdvisories: diagnostics.some((item) => item.level === "info"),
|
|
2698
|
+
ok: diagnostics.every((item) => item.level !== "error" && item.level !== "fatal")
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
async function validateProject(options = {}) {
|
|
2702
|
+
const rootDir = path21.resolve(options.rootDir ?? process.cwd());
|
|
2703
|
+
const snapshot = await collectDoctorSnapshot(rootDir);
|
|
2704
|
+
const diagnostics = [];
|
|
2705
|
+
if (snapshot.intent.runtime === "node" && !snapshot.files.has("package.json")) {
|
|
2706
|
+
diagnostics.push({
|
|
2707
|
+
code: "VALIDATION_NODE_PACKAGE_JSON_MISSING",
|
|
2708
|
+
level: "error",
|
|
2709
|
+
subsystem: "validation",
|
|
2710
|
+
ruleId: "project.node-package-json",
|
|
2711
|
+
filePath: "package.json",
|
|
2712
|
+
message: "The project declares the Node.js runtime but package.json is missing.",
|
|
2713
|
+
hint: "Add package.json or correct the runtime declaration in .appinit/context.json."
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
if (snapshot.intent.runtime === "deno" && !snapshot.files.has("deno.json") && !snapshot.files.has("deno.jsonc")) {
|
|
2717
|
+
diagnostics.push({
|
|
2718
|
+
code: "VALIDATION_DENO_CONFIG_MISSING",
|
|
2719
|
+
level: "error",
|
|
2720
|
+
subsystem: "validation",
|
|
2721
|
+
ruleId: "project.deno-config",
|
|
2722
|
+
message: "The project declares the Deno runtime but no Deno configuration file was found.",
|
|
2723
|
+
hint: "Add deno.json or deno.jsonc, or correct the runtime declaration."
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
return createReport(rootDir, diagnostics);
|
|
2727
|
+
}
|
|
2728
|
+
async function validateTemplate(options) {
|
|
2729
|
+
const rootDir = path21.resolve(options.templateDir);
|
|
2730
|
+
const diagnostics = [];
|
|
2731
|
+
try {
|
|
2732
|
+
await resolveTemplate(rootDir);
|
|
2733
|
+
} catch (error) {
|
|
2734
|
+
diagnostics.push({
|
|
2735
|
+
code: "VALIDATION_TEMPLATE_INVALID",
|
|
2736
|
+
level: "error",
|
|
2737
|
+
subsystem: "template",
|
|
2738
|
+
ruleId: "template.resolve",
|
|
2739
|
+
filePath: "appinit.template.json",
|
|
2740
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2741
|
+
hint: "Check the template manifest and ensure its declared template directory exists within the template root."
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
return createReport(rootDir, diagnostics);
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// src/doctor.ts
|
|
2748
|
+
async function runDoctorCommand(options = {}, io = { write: (message) => process.stdout.write(message) }) {
|
|
2749
|
+
const report = await new Doctor({ rootDir: path22.resolve(options.cwd ?? process.cwd()) }).diagnose();
|
|
2750
|
+
if (options.json) {
|
|
2751
|
+
io.write(`${JSON.stringify(report, null, 2)}
|
|
2752
|
+
`);
|
|
2753
|
+
return;
|
|
2754
|
+
}
|
|
2755
|
+
io.write("AppInit Doctor\n\n");
|
|
2756
|
+
if (!report.diagnostics.length) io.write(" \u2713 No project issues detected.\n");
|
|
2757
|
+
for (const item of report.diagnostics) {
|
|
2758
|
+
const marker = item.level === "info" ? "i" : item.level === "warn" ? "!" : "\u2717";
|
|
2759
|
+
io.write(` ${marker} [${item.level.toUpperCase()}] ${item.code}: ${item.message}
|
|
2760
|
+
`);
|
|
2761
|
+
if (item.hint) io.write(` Hint: ${item.hint}
|
|
2762
|
+
`);
|
|
2763
|
+
}
|
|
2764
|
+
io.write(`
|
|
2765
|
+
Summary: ${report.summary.info} info, ${report.summary.warn} warnings, ${report.summary.error} errors
|
|
2766
|
+
`);
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
// src/history.ts
|
|
2770
|
+
async function readProjectHistory() {
|
|
2771
|
+
const value = await readAppInitJson(resolveAppInitHome(), "state.json");
|
|
2772
|
+
if (value === void 0) return { schemaVersion: 1, projects: [] };
|
|
2773
|
+
const validation = validateAppInitState(value);
|
|
2774
|
+
if (!validation.valid || !validation.state) {
|
|
2775
|
+
throw new Error(`Global AppInit history is invalid: ${validation.errors.join(" ")}`);
|
|
2776
|
+
}
|
|
2777
|
+
return validation.state;
|
|
2778
|
+
}
|
|
2779
|
+
async function runHistoryCommand(options = {}, io = { write: (message) => process.stdout.write(message) }) {
|
|
2780
|
+
const state = await readProjectHistory();
|
|
2781
|
+
const projects = [...state.projects].sort(
|
|
2782
|
+
(left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt)
|
|
2783
|
+
);
|
|
2784
|
+
if (options.json) {
|
|
2785
|
+
io.write(`${JSON.stringify({ schemaVersion: state.schemaVersion, projects }, null, 2)}
|
|
2786
|
+
`);
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
if (projects.length === 0) {
|
|
2790
|
+
io.write("No project history found. Create a project with `appinit create` to get started.\n");
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
io.write(`AppInit Project History (${projects.length})
|
|
2794
|
+
|
|
2795
|
+
`);
|
|
2796
|
+
projects.forEach((project, index) => {
|
|
2797
|
+
io.write(
|
|
2798
|
+
`${index + 1}. ${project.templateId}
|
|
2799
|
+
Path: ${project.path}
|
|
2800
|
+
Created: ${project.createdAt}
|
|
2801
|
+
|
|
2802
|
+
`
|
|
2803
|
+
);
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// src/state-path.ts
|
|
2808
|
+
import { stat as stat3 } from "fs/promises";
|
|
2809
|
+
async function inspectAppInitStatePath(options = {}) {
|
|
2810
|
+
const env = options.env ?? process.env;
|
|
2811
|
+
const resolvedPath = resolveAppInitHome({ env, homeDirectory: options.homeDirectory });
|
|
2812
|
+
let exists2 = false;
|
|
2813
|
+
let isDirectory = false;
|
|
2814
|
+
try {
|
|
2815
|
+
const details = await stat3(resolvedPath);
|
|
2816
|
+
exists2 = true;
|
|
2817
|
+
isDirectory = details.isDirectory();
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
if (error.code !== "ENOENT") throw error;
|
|
2820
|
+
}
|
|
2821
|
+
return Object.freeze({
|
|
2822
|
+
path: resolvedPath,
|
|
2823
|
+
source: env.APPINIT_HOME === void 0 ? "default" : "APPINIT_HOME",
|
|
2824
|
+
exists: exists2,
|
|
2825
|
+
isDirectory
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
async function runStatePathCommand(options = {}, io = { write: (message) => process.stdout.write(message) }) {
|
|
2829
|
+
const info = await inspectAppInitStatePath(options);
|
|
2830
|
+
io.write("AppInit Global State Path\n\n");
|
|
2831
|
+
io.write(`Path: ${info.path}
|
|
2832
|
+
`);
|
|
2833
|
+
io.write(`Source: ${info.source === "default" ? "default (~/.appinit)" : "APPINIT_HOME environment variable"}
|
|
2834
|
+
`);
|
|
2835
|
+
if (!info.exists) {
|
|
2836
|
+
io.write("Status: not created yet\n");
|
|
2837
|
+
} else if (!info.isDirectory) {
|
|
2838
|
+
io.write("Status: path exists but is not a directory\n");
|
|
2839
|
+
} else {
|
|
2840
|
+
io.write("Status: directory exists\n");
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// src/state-inspect.ts
|
|
2845
|
+
async function inspectAppInitState(options = {}) {
|
|
2846
|
+
const env = options.env ?? process.env;
|
|
2847
|
+
const home = resolveAppInitHome({ env, homeDirectory: options.homeDirectory });
|
|
2848
|
+
const diagnostics = [];
|
|
2849
|
+
try {
|
|
2850
|
+
const value = await readAppInitJson(home, "state.json");
|
|
2851
|
+
if (value === void 0) {
|
|
2852
|
+
diagnostics.push("Global state file has not been created yet.");
|
|
2853
|
+
return Object.freeze({
|
|
2854
|
+
path: home,
|
|
2855
|
+
source: env.APPINIT_HOME === void 0 ? "default" : "APPINIT_HOME",
|
|
2856
|
+
stateFile: "missing",
|
|
2857
|
+
diagnostics: Object.freeze(diagnostics)
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
const validation = validateAppInitState(value);
|
|
2861
|
+
if (!validation.valid || !validation.state) {
|
|
2862
|
+
diagnostics.push(...validation.errors);
|
|
2863
|
+
return Object.freeze({
|
|
2864
|
+
path: home,
|
|
2865
|
+
source: env.APPINIT_HOME === void 0 ? "default" : "APPINIT_HOME",
|
|
2866
|
+
stateFile: "invalid",
|
|
2867
|
+
diagnostics: Object.freeze(diagnostics)
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
return Object.freeze({
|
|
2871
|
+
path: home,
|
|
2872
|
+
source: env.APPINIT_HOME === void 0 ? "default" : "APPINIT_HOME",
|
|
2873
|
+
stateFile: "valid",
|
|
2874
|
+
schemaVersion: validation.state.schemaVersion,
|
|
2875
|
+
projectCount: validation.state.projects.length,
|
|
2876
|
+
diagnostics: Object.freeze(diagnostics)
|
|
2877
|
+
});
|
|
2878
|
+
} catch (error) {
|
|
2879
|
+
diagnostics.push(error instanceof Error ? error.message : String(error));
|
|
2880
|
+
return Object.freeze({
|
|
2881
|
+
path: home,
|
|
2882
|
+
source: env.APPINIT_HOME === void 0 ? "default" : "APPINIT_HOME",
|
|
2883
|
+
stateFile: "invalid",
|
|
2884
|
+
diagnostics: Object.freeze(diagnostics)
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
async function runStateInspectCommand(options = {}, io = { write: (message) => process.stdout.write(message) }) {
|
|
2889
|
+
const report = await inspectAppInitState(options);
|
|
2890
|
+
if (options.json) {
|
|
2891
|
+
io.write(`${JSON.stringify(report, null, 2)}
|
|
2892
|
+
`);
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
io.write("AppInit Global State Inspection\n\n");
|
|
2896
|
+
io.write(`Path: ${report.path}
|
|
2897
|
+
`);
|
|
2898
|
+
io.write(`Source: ${report.source === "default" ? "default (~/.appinit)" : "APPINIT_HOME environment variable"}
|
|
2899
|
+
`);
|
|
2900
|
+
io.write(`State file: ${report.stateFile}
|
|
2901
|
+
`);
|
|
2902
|
+
if (report.schemaVersion !== void 0) io.write(`Schema version: ${report.schemaVersion}
|
|
2903
|
+
`);
|
|
2904
|
+
if (report.projectCount !== void 0) io.write(`Recorded projects: ${report.projectCount}
|
|
2905
|
+
`);
|
|
2906
|
+
if (report.diagnostics.length) {
|
|
2907
|
+
io.write("\nDiagnostics\n");
|
|
2908
|
+
for (const diagnostic3 of report.diagnostics) io.write(` ! ${diagnostic3}
|
|
2909
|
+
`);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
// src/state-doctor.ts
|
|
2914
|
+
import { access as access3, stat as stat4 } from "fs/promises";
|
|
2915
|
+
import { constants } from "fs";
|
|
2916
|
+
import path23 from "path";
|
|
2917
|
+
function diagnostic2(code, level, message, hint) {
|
|
2918
|
+
return Object.freeze({ code, level, message, ...hint ? { hint } : {} });
|
|
2919
|
+
}
|
|
2920
|
+
function isMissing(error) {
|
|
2921
|
+
return error.code === "ENOENT";
|
|
2922
|
+
}
|
|
2923
|
+
async function diagnoseAppInitState(options = {}) {
|
|
2924
|
+
const inspection = await inspectAppInitState(options);
|
|
2925
|
+
const diagnostics = [];
|
|
2926
|
+
const stateRoot = inspection.path;
|
|
2927
|
+
let rootIsDirectory = false;
|
|
2928
|
+
try {
|
|
2929
|
+
const details = await stat4(stateRoot);
|
|
2930
|
+
if (!details.isDirectory()) {
|
|
2931
|
+
diagnostics.push(diagnostic2(
|
|
2932
|
+
"APPINIT_STATE_HOME_NOT_DIRECTORY",
|
|
2933
|
+
"error",
|
|
2934
|
+
"The configured global state path exists but is not a directory.",
|
|
2935
|
+
"Set APPINIT_HOME to a directory or remove the conflicting file."
|
|
2936
|
+
));
|
|
2937
|
+
} else {
|
|
2938
|
+
rootIsDirectory = true;
|
|
2939
|
+
try {
|
|
2940
|
+
await access3(stateRoot, constants.R_OK | constants.X_OK);
|
|
2941
|
+
} catch {
|
|
2942
|
+
diagnostics.push(diagnostic2(
|
|
2943
|
+
"APPINIT_STATE_HOME_NOT_ACCESSIBLE",
|
|
2944
|
+
"error",
|
|
2945
|
+
"The global state directory is not readable or searchable by this process.",
|
|
2946
|
+
"Check directory ownership and access permissions."
|
|
2947
|
+
));
|
|
2948
|
+
}
|
|
2949
|
+
if (process.platform !== "win32" && (details.mode & 63) !== 0) {
|
|
2950
|
+
diagnostics.push(diagnostic2(
|
|
2951
|
+
"APPINIT_STATE_HOME_PERMISSIONS_BROAD",
|
|
2952
|
+
"warn",
|
|
2953
|
+
"The global state directory grants permissions to group or other users.",
|
|
2954
|
+
"Restrict the directory to the current user (typically mode 700)."
|
|
2955
|
+
));
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
} catch (error) {
|
|
2959
|
+
if (isMissing(error)) {
|
|
2960
|
+
diagnostics.push(diagnostic2(
|
|
2961
|
+
"APPINIT_STATE_HOME_MISSING",
|
|
2962
|
+
"warn",
|
|
2963
|
+
"The global state directory has not been created yet.",
|
|
2964
|
+
"This is normal before AppInit first stores global state."
|
|
2965
|
+
));
|
|
2966
|
+
} else {
|
|
2967
|
+
diagnostics.push(diagnostic2(
|
|
2968
|
+
"APPINIT_STATE_HOME_UNAVAILABLE",
|
|
2969
|
+
"error",
|
|
2970
|
+
"The global state directory could not be inspected.",
|
|
2971
|
+
"Check the configured path and filesystem permissions."
|
|
2972
|
+
));
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
if (inspection.stateFile === "missing") {
|
|
2976
|
+
diagnostics.push(diagnostic2(
|
|
2977
|
+
"APPINIT_STATE_FILE_MISSING",
|
|
2978
|
+
"warn",
|
|
2979
|
+
"The global state file has not been created yet.",
|
|
2980
|
+
"AppInit will create it when a command first needs to persist global state."
|
|
2981
|
+
));
|
|
2982
|
+
} else if (inspection.stateFile === "invalid") {
|
|
2983
|
+
diagnostics.push(diagnostic2(
|
|
2984
|
+
"APPINIT_STATE_FILE_INVALID",
|
|
2985
|
+
"error",
|
|
2986
|
+
inspection.diagnostics.join(" "),
|
|
2987
|
+
"Back up the file before attempting any manual recovery; this command does not repair state."
|
|
2988
|
+
));
|
|
2989
|
+
} else {
|
|
2990
|
+
diagnostics.push(diagnostic2(
|
|
2991
|
+
"APPINIT_STATE_FILE_VALID",
|
|
2992
|
+
"info",
|
|
2993
|
+
`Global state file is valid (schema version ${inspection.schemaVersion}; ${inspection.projectCount} recorded projects).`
|
|
2994
|
+
));
|
|
2995
|
+
if (rootIsDirectory) {
|
|
2996
|
+
const filePath = path23.join(stateRoot, "state.json");
|
|
2997
|
+
try {
|
|
2998
|
+
const details = await stat4(filePath);
|
|
2999
|
+
await access3(filePath, constants.R_OK);
|
|
3000
|
+
if (process.platform !== "win32" && (details.mode & 63) !== 0) {
|
|
3001
|
+
diagnostics.push(diagnostic2(
|
|
3002
|
+
"APPINIT_STATE_FILE_PERMISSIONS_BROAD",
|
|
3003
|
+
"warn",
|
|
3004
|
+
"The global state file grants permissions to group or other users.",
|
|
3005
|
+
"Restrict the file to the current user (typically mode 600)."
|
|
3006
|
+
));
|
|
3007
|
+
}
|
|
3008
|
+
} catch {
|
|
3009
|
+
diagnostics.push(diagnostic2(
|
|
3010
|
+
"APPINIT_STATE_FILE_NOT_ACCESSIBLE",
|
|
3011
|
+
"error",
|
|
3012
|
+
"The global state file could not be inspected or read.",
|
|
3013
|
+
"Check file ownership and access permissions."
|
|
3014
|
+
));
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
const summary = Object.freeze({
|
|
3019
|
+
info: diagnostics.filter((item) => item.level === "info").length,
|
|
3020
|
+
warn: diagnostics.filter((item) => item.level === "warn").length,
|
|
3021
|
+
error: diagnostics.filter((item) => item.level === "error").length
|
|
3022
|
+
});
|
|
3023
|
+
return Object.freeze({
|
|
3024
|
+
path: stateRoot,
|
|
3025
|
+
source: inspection.source,
|
|
3026
|
+
stateFile: inspection.stateFile,
|
|
3027
|
+
ok: summary.error === 0,
|
|
3028
|
+
diagnostics: Object.freeze(diagnostics),
|
|
3029
|
+
summary
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
3032
|
+
async function runStateDoctorCommand(options = {}, io = { write: (message) => process.stdout.write(message) }) {
|
|
3033
|
+
const report = await diagnoseAppInitState(options);
|
|
3034
|
+
if (options.json) {
|
|
3035
|
+
io.write(`${JSON.stringify(report, null, 2)}
|
|
3036
|
+
`);
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
io.write("AppInit Global State Doctor\n\n");
|
|
3040
|
+
io.write(`Path: ${report.path}
|
|
3041
|
+
`);
|
|
3042
|
+
io.write(`Source: ${report.source === "default" ? "default (~/.appinit)" : "APPINIT_HOME environment variable"}
|
|
3043
|
+
`);
|
|
3044
|
+
for (const item of report.diagnostics) {
|
|
3045
|
+
const marker = item.level === "info" ? "i" : item.level === "warn" ? "!" : "\u2717";
|
|
3046
|
+
io.write(` ${marker} [${item.level.toUpperCase()}] ${item.code}: ${item.message}
|
|
3047
|
+
`);
|
|
3048
|
+
if (item.hint) io.write(` Hint: ${item.hint}
|
|
3049
|
+
`);
|
|
3050
|
+
}
|
|
3051
|
+
io.write(`
|
|
3052
|
+
Summary: ${report.summary.info} info, ${report.summary.warn} warnings, ${report.summary.error} errors
|
|
3053
|
+
`);
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
// src/template-author.ts
|
|
3057
|
+
import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
|
|
3058
|
+
import path24 from "path";
|
|
3059
|
+
function toSlug(value) {
|
|
3060
|
+
return value.toLocaleLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3061
|
+
}
|
|
3062
|
+
function toDisplayName(slug) {
|
|
3063
|
+
return slug.split("-").filter(Boolean).map((part) => part.charAt(0).toLocaleUpperCase() + part.slice(1)).join(" ");
|
|
3064
|
+
}
|
|
3065
|
+
async function createTemplateScaffold(directory) {
|
|
3066
|
+
const root = path24.resolve(directory);
|
|
3067
|
+
const slug = toSlug(path24.basename(root));
|
|
3068
|
+
if (!slug) throw new Error("Template directory name must contain letters or numbers.");
|
|
3069
|
+
const name = toDisplayName(slug);
|
|
3070
|
+
const id = `custom.template-${slug}`;
|
|
3071
|
+
const manifest = {
|
|
3072
|
+
schemaVersion: 1,
|
|
3073
|
+
id,
|
|
3074
|
+
version: "0.1.0",
|
|
3075
|
+
kind: "project",
|
|
3076
|
+
atsVersion: "2.0",
|
|
3077
|
+
name,
|
|
3078
|
+
description: "A reusable AppInit project template.",
|
|
3079
|
+
category: "custom",
|
|
3080
|
+
language: "typescript",
|
|
3081
|
+
packageName: `@appinit/template-${slug}`,
|
|
3082
|
+
renderer: { engine: "liquid" },
|
|
3083
|
+
paths: { template: "template", variables: "variables" },
|
|
3084
|
+
tags: ["custom", "typescript"]
|
|
3085
|
+
};
|
|
3086
|
+
await mkdir5(path24.dirname(root), { recursive: true });
|
|
3087
|
+
await mkdir5(root);
|
|
3088
|
+
await mkdir5(path24.join(root, "template"));
|
|
3089
|
+
await mkdir5(path24.join(root, "variables"));
|
|
3090
|
+
await Promise.all([
|
|
3091
|
+
writeFile4(path24.join(root, "appinit.template.json"), `${JSON.stringify(manifest, null, 2)}
|
|
3092
|
+
`, { flag: "wx" }),
|
|
3093
|
+
writeFile4(path24.join(root, "variables", "defaults.json"), `${JSON.stringify({ projectName: slug }, null, 2)}
|
|
3094
|
+
`, { flag: "wx" }),
|
|
3095
|
+
writeFile4(path24.join(root, "variables", "schema.json"), `${JSON.stringify({
|
|
3096
|
+
projectName: { type: "string", required: true, minLength: 1 }
|
|
3097
|
+
}, null, 2)}
|
|
3098
|
+
`, { flag: "wx" }),
|
|
3099
|
+
writeFile4(path24.join(root, "template", "README.md"), `# {{ projectName }}
|
|
3100
|
+
|
|
3101
|
+
Generated from the ${name} AppInit template.
|
|
3102
|
+
`, { flag: "wx" }),
|
|
3103
|
+
writeFile4(path24.join(root, "README.md"), `# ${name}
|
|
3104
|
+
|
|
3105
|
+
AppInit template authoring scaffold.
|
|
3106
|
+
|
|
3107
|
+
- Edit \`appinit.template.json\` to update template metadata.
|
|
3108
|
+
- Add generated files under \`template/\`.
|
|
3109
|
+
- Define variable defaults and rules under \`variables/\`.
|
|
3110
|
+
- Validate this directory with \`appinit validate --template .\`.
|
|
3111
|
+
|
|
3112
|
+
Preview generation with \`appinit create <template-ref> <target> --dry-run\`.
|
|
3113
|
+
`, { flag: "wx" })
|
|
3114
|
+
]);
|
|
3115
|
+
return Object.freeze({ root, id, name });
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
// src/template-commands.ts
|
|
3119
|
+
import path25 from "path";
|
|
3120
|
+
function searchLocalTemplates(templates, query) {
|
|
3121
|
+
const terms = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
|
|
3122
|
+
if (terms.length === 0) return templates;
|
|
3123
|
+
return templates.filter((template) => {
|
|
3124
|
+
const manifest = template.template.manifest;
|
|
3125
|
+
const searchable = [
|
|
3126
|
+
template.id,
|
|
3127
|
+
template.name,
|
|
3128
|
+
template.description,
|
|
3129
|
+
template.category,
|
|
3130
|
+
manifest.language,
|
|
3131
|
+
...manifest.tags
|
|
3132
|
+
].join(" ").toLocaleLowerCase();
|
|
3133
|
+
return terms.every((term) => searchable.includes(term));
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
function findLocalTemplate(templates, reference) {
|
|
3137
|
+
const normalized = reference.trim().toLocaleLowerCase();
|
|
3138
|
+
return templates.find(
|
|
3139
|
+
(template) => [
|
|
3140
|
+
template.id,
|
|
3141
|
+
template.name,
|
|
3142
|
+
template.template.manifest.packageName,
|
|
3143
|
+
path25.basename(template.root)
|
|
3144
|
+
].some((value) => value?.toLocaleLowerCase() === normalized)
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
function formatTemplate(template) {
|
|
3148
|
+
const manifest = template.template.manifest;
|
|
3149
|
+
return [
|
|
3150
|
+
` ${template.id}`,
|
|
3151
|
+
` ${template.name} \u2014 ${template.description}`,
|
|
3152
|
+
` ${template.category} \xB7 ${manifest.language} \xB7 v${manifest.version}`
|
|
3153
|
+
].join("\n");
|
|
3154
|
+
}
|
|
3155
|
+
function formatTemplateList(templates, heading = "Available local templates") {
|
|
3156
|
+
if (templates.length === 0)
|
|
3157
|
+
return "No local templates found. Check your templates directory.";
|
|
3158
|
+
return [
|
|
3159
|
+
`${heading} (${templates.length})`,
|
|
3160
|
+
"",
|
|
3161
|
+
...templates.map(formatTemplate),
|
|
3162
|
+
""
|
|
3163
|
+
].join("\n");
|
|
3164
|
+
}
|
|
3165
|
+
function formatTemplateInfo(template) {
|
|
3166
|
+
const manifest = template.template.manifest;
|
|
3167
|
+
return [
|
|
3168
|
+
template.name,
|
|
3169
|
+
"",
|
|
3170
|
+
`ID: ${template.id}`,
|
|
3171
|
+
`Version: ${manifest.version}`,
|
|
3172
|
+
`Category: ${template.category}`,
|
|
3173
|
+
`Language: ${manifest.language}`,
|
|
3174
|
+
`Description: ${template.description}`,
|
|
3175
|
+
`Tags: ${manifest.tags.join(", ") || "\u2014"}`,
|
|
3176
|
+
`Directory: ${template.root}`,
|
|
3177
|
+
""
|
|
3178
|
+
].join("\n");
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
// src/remote-template.ts
|
|
3182
|
+
import { mkdir as mkdir6, realpath as realpath8, rm as rm5 } from "fs/promises";
|
|
3183
|
+
import path26 from "path";
|
|
3184
|
+
function createClient(options) {
|
|
3185
|
+
return new TemplateRegistryClient({
|
|
3186
|
+
catalogUrl: options.registryUrl,
|
|
3187
|
+
cacheDirectory: options.cacheDirectory ?? path26.join(resolveAppInitHome(), "registry-cache"),
|
|
3188
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
3189
|
+
});
|
|
3190
|
+
}
|
|
3191
|
+
async function listRemoteTemplates(options) {
|
|
3192
|
+
const catalog = await createClient(options).fetchCatalog();
|
|
3193
|
+
return Object.freeze(catalog.templates.map((entry) => Object.freeze({
|
|
3194
|
+
id: entry.id,
|
|
3195
|
+
version: entry.version,
|
|
3196
|
+
name: entry.name,
|
|
3197
|
+
description: entry.description,
|
|
3198
|
+
category: entry.category,
|
|
3199
|
+
language: entry.language,
|
|
3200
|
+
tags: entry.tags
|
|
3201
|
+
})));
|
|
3202
|
+
}
|
|
3203
|
+
async function installRemoteTemplate(options) {
|
|
3204
|
+
const client = createClient(options);
|
|
3205
|
+
const catalog = await client.fetchCatalog();
|
|
3206
|
+
const candidates = catalog.templates.filter(
|
|
3207
|
+
(entry2) => entry2.id === options.templateId && (!options.version || entry2.version === options.version)
|
|
3208
|
+
);
|
|
3209
|
+
if (candidates.length === 0) {
|
|
3210
|
+
throw new Error(`Template "${options.templateId}"${options.version ? ` version "${options.version}"` : ""} was not found in the registry.`);
|
|
3211
|
+
}
|
|
3212
|
+
if (candidates.length !== 1) {
|
|
3213
|
+
throw new Error(`Registry contains multiple versions for "${options.templateId}". Specify --version explicitly.`);
|
|
3214
|
+
}
|
|
3215
|
+
const entry = candidates[0];
|
|
3216
|
+
const archive = await client.downloadTemplate(entry);
|
|
3217
|
+
const templatesDirectory = path26.resolve(
|
|
3218
|
+
options.templatesDirectory ?? path26.join(resolveAppInitHome(), "templates")
|
|
3219
|
+
);
|
|
3220
|
+
const destination = path26.join(templatesDirectory, entry.id, entry.version);
|
|
3221
|
+
await mkdir6(templatesDirectory, { recursive: true });
|
|
3222
|
+
let extractedDirectoryCreated = false;
|
|
3223
|
+
try {
|
|
3224
|
+
const extracted = await extractTemplateArchive({
|
|
3225
|
+
archivePath: archive.path,
|
|
3226
|
+
destination
|
|
3227
|
+
});
|
|
3228
|
+
extractedDirectoryCreated = true;
|
|
3229
|
+
const template = await resolveTemplate(extracted.destination);
|
|
3230
|
+
if (template.manifest.id !== entry.id || template.manifest.version !== entry.version) {
|
|
3231
|
+
throw new Error(
|
|
3232
|
+
`Downloaded manifest identity mismatch: expected ${entry.id}@${entry.version}, received ${template.manifest.id}@${template.manifest.version}.`
|
|
3233
|
+
);
|
|
3234
|
+
}
|
|
3235
|
+
const canonical = await realpath8(extracted.destination);
|
|
3236
|
+
return Object.freeze({
|
|
3237
|
+
id: entry.id,
|
|
3238
|
+
version: entry.version,
|
|
3239
|
+
directory: canonical,
|
|
3240
|
+
cached: archive.cached
|
|
3241
|
+
});
|
|
3242
|
+
} catch (error) {
|
|
3243
|
+
if (extractedDirectoryCreated) {
|
|
3244
|
+
await rm5(destination, { recursive: true, force: true });
|
|
3245
|
+
}
|
|
3246
|
+
throw error;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// src/template.ts
|
|
3251
|
+
function output(value, json) {
|
|
3252
|
+
return json ? `${JSON.stringify(value, null, 2)}
|
|
3253
|
+
` : String(value);
|
|
3254
|
+
}
|
|
3255
|
+
function usage() {
|
|
3256
|
+
return [
|
|
3257
|
+
"Usage: appinit template <init|list|info|search|remote-list|install> [arguments] [options]",
|
|
3258
|
+
"",
|
|
3259
|
+
"Commands:",
|
|
3260
|
+
" init <directory> Create a starter template authoring scaffold",
|
|
3261
|
+
" list List validated local templates",
|
|
3262
|
+
" info <template-ref> Show details and variable configuration",
|
|
3263
|
+
" search <query> Search local templates by name, ID, tags, and description",
|
|
3264
|
+
" remote-list List templates from a remote registry",
|
|
3265
|
+
" install <template-id> Download, verify, and install a registry template",
|
|
3266
|
+
"",
|
|
3267
|
+
"Options:",
|
|
3268
|
+
" --json Print machine-readable JSON",
|
|
3269
|
+
" --template-dir <path> Use a specific local template directory",
|
|
3270
|
+
" --registry-url <url> HTTPS registry catalog URL (required for remote commands)",
|
|
3271
|
+
" --version <version> Install this exact template version",
|
|
3272
|
+
""
|
|
3273
|
+
].join("\n");
|
|
3274
|
+
}
|
|
3275
|
+
function parseOptions(args) {
|
|
3276
|
+
const positional = [];
|
|
3277
|
+
let json = false;
|
|
3278
|
+
let directory;
|
|
3279
|
+
let registryUrl;
|
|
3280
|
+
let version;
|
|
3281
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3282
|
+
const arg = args[index];
|
|
3283
|
+
if (arg === "--json") json = true;
|
|
3284
|
+
else if (arg === "--help" || arg === "-h") positional.push("help");
|
|
3285
|
+
else if (arg === "--template-dir" || arg === "--registry-url" || arg === "--version") {
|
|
3286
|
+
const value = args[++index];
|
|
3287
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg} requires a value.`);
|
|
3288
|
+
if (arg === "--template-dir") directory = value;
|
|
3289
|
+
else if (arg === "--registry-url") registryUrl = value;
|
|
3290
|
+
else version = value;
|
|
3291
|
+
} else if (arg.startsWith("-")) throw new Error(`Unknown template option: ${arg}`);
|
|
3292
|
+
else positional.push(arg);
|
|
3293
|
+
}
|
|
3294
|
+
return {
|
|
3295
|
+
positional,
|
|
3296
|
+
json,
|
|
3297
|
+
...directory ? { directory } : {},
|
|
3298
|
+
...registryUrl ? { registryUrl } : {},
|
|
3299
|
+
...version ? { version } : {}
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
async function runTemplateCommand(args, options = {}) {
|
|
3303
|
+
const write = options.write ?? ((message) => process.stdout.write(message));
|
|
3304
|
+
const parsed = parseOptions(args);
|
|
3305
|
+
const [subcommand, ...rest] = parsed.positional;
|
|
3306
|
+
if (!subcommand || subcommand === "--help" || subcommand === "help") {
|
|
3307
|
+
write(usage());
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
if (!["init", "list", "info", "search", "remote-list", "install"].includes(subcommand)) {
|
|
3311
|
+
throw new Error(`Unknown template command "${subcommand}". Run "appinit template --help".`);
|
|
3312
|
+
}
|
|
3313
|
+
if (subcommand === "remote-list" || subcommand === "install") {
|
|
3314
|
+
if (!parsed.registryUrl) throw new Error(`${subcommand} requires --registry-url <https-url>.`);
|
|
3315
|
+
if (parsed.directory) throw new Error(`${subcommand} does not use --template-dir.`);
|
|
3316
|
+
if (subcommand === "remote-list") {
|
|
3317
|
+
if (rest.length) throw new Error("template remote-list does not accept positional arguments.");
|
|
3318
|
+
if (parsed.version) throw new Error("template remote-list does not accept --version.");
|
|
3319
|
+
const entries = await listRemoteTemplates({ registryUrl: parsed.registryUrl });
|
|
3320
|
+
if (parsed.json) write(output(entries, true));
|
|
3321
|
+
else if (!entries.length) write("No remote templates found.\n");
|
|
3322
|
+
else write(entries.map(
|
|
3323
|
+
(item) => `${item.id}@${item.version} ${item.name}
|
|
3324
|
+
${item.description}
|
|
3325
|
+
${item.category} \xB7 ${item.language} \xB7 ${item.tags.join(", ")}`
|
|
3326
|
+
).join("\n\n") + "\n");
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
if (rest.length !== 1) throw new Error("template install requires exactly one template ID.");
|
|
3330
|
+
const installed = await installRemoteTemplate({
|
|
3331
|
+
registryUrl: parsed.registryUrl,
|
|
3332
|
+
templateId: rest[0],
|
|
3333
|
+
...parsed.version ? { version: parsed.version } : {}
|
|
3334
|
+
});
|
|
3335
|
+
if (parsed.json) write(output(installed, true));
|
|
3336
|
+
else write(`Installed ${installed.id}@${installed.version}
|
|
3337
|
+
Directory: ${installed.directory}
|
|
3338
|
+
Cache: ${installed.cached ? "verified archive reused" : "downloaded and verified"}
|
|
3339
|
+
`);
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
if (parsed.registryUrl || parsed.version) {
|
|
3343
|
+
throw new Error("--registry-url and --version are only valid with template remote-list or template install.");
|
|
3344
|
+
}
|
|
3345
|
+
if (subcommand === "init") {
|
|
3346
|
+
if (rest.length !== 1) throw new Error("template init requires exactly one target directory.");
|
|
3347
|
+
const scaffold = await createTemplateScaffold(rest[0]);
|
|
3348
|
+
if (parsed.json) write(output(scaffold, true));
|
|
3349
|
+
else write(`Created template scaffold at ${scaffold.root}
|
|
3350
|
+
|
|
3351
|
+
Template ID: ${scaffold.id}
|
|
3352
|
+
Next: edit the manifest and files, then run appinit validate --template ${scaffold.root}
|
|
3353
|
+
`);
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
const directory = resolveTemplateDirectory(parsed.directory ?? options.templateDirectory);
|
|
3357
|
+
const templates = await discoverLocalTemplates(directory);
|
|
3358
|
+
if (subcommand === "list") {
|
|
3359
|
+
if (rest.length) throw new Error("template list does not accept positional arguments.");
|
|
3360
|
+
const entries = templates.map(({ id, name, description, category, template }) => ({
|
|
3361
|
+
id,
|
|
3362
|
+
name,
|
|
3363
|
+
version: template.manifest.version,
|
|
3364
|
+
category,
|
|
3365
|
+
description
|
|
3366
|
+
}));
|
|
3367
|
+
if (parsed.json) write(output(entries, true));
|
|
3368
|
+
else if (!entries.length) write("No valid local templates found.\n");
|
|
3369
|
+
else write(entries.map((item) => `${item.id} ${item.name} (${item.version})
|
|
3370
|
+
${item.description}`).join("\n\n") + "\n");
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
if (subcommand === "search") {
|
|
3374
|
+
const query = rest.join(" ").trim();
|
|
3375
|
+
if (!query) throw new Error("template search requires a query.");
|
|
3376
|
+
const matches = searchLocalTemplates(templates, query);
|
|
3377
|
+
const entries = matches.map(({ id, name, description, category, template }) => ({
|
|
3378
|
+
id,
|
|
3379
|
+
name,
|
|
3380
|
+
version: template.manifest.version,
|
|
3381
|
+
category,
|
|
3382
|
+
description
|
|
3383
|
+
}));
|
|
3384
|
+
if (parsed.json) write(output(entries, true));
|
|
3385
|
+
else if (!entries.length) write(`No templates found for "${query}".
|
|
3386
|
+
`);
|
|
3387
|
+
else write(entries.map((item) => `${item.id} ${item.name} (${item.version})
|
|
3388
|
+
${item.description}`).join("\n\n") + "\n");
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
if (rest.length !== 1) throw new Error("template info requires exactly one template reference.");
|
|
3392
|
+
const ref = rest[0];
|
|
3393
|
+
const selected = findLocalTemplate(templates, ref);
|
|
3394
|
+
if (!selected) throw new Error(`Template "${ref}" was not found among valid local templates.`);
|
|
3395
|
+
const variables = await loadTemplateVariables(selected.root, selected.template.manifest);
|
|
3396
|
+
const details = {
|
|
3397
|
+
id: selected.id,
|
|
3398
|
+
name: selected.name,
|
|
3399
|
+
version: selected.template.manifest.version,
|
|
3400
|
+
description: selected.description,
|
|
3401
|
+
category: selected.category,
|
|
3402
|
+
kind: selected.template.manifest.kind,
|
|
3403
|
+
language: selected.template.manifest.language,
|
|
3404
|
+
packageName: selected.template.manifest.packageName,
|
|
3405
|
+
atsVersion: selected.template.manifest.atsVersion,
|
|
3406
|
+
tags: selected.template.manifest.tags,
|
|
3407
|
+
renderer: selected.template.manifest.renderer.engine,
|
|
3408
|
+
variables: { defaults: variables.defaults, schema: variables.schema }
|
|
3409
|
+
};
|
|
3410
|
+
if (parsed.json) write(output(details, true));
|
|
3411
|
+
else {
|
|
3412
|
+
write([
|
|
3413
|
+
`${details.name} (${details.id})`,
|
|
3414
|
+
details.description,
|
|
3415
|
+
`Version: ${details.version}`,
|
|
3416
|
+
`Category: ${details.category} | Kind: ${details.kind} | Language: ${details.language}`,
|
|
3417
|
+
`Package: ${details.packageName}`,
|
|
3418
|
+
`ATS compatibility: ${details.atsVersion}`,
|
|
3419
|
+
`Renderer: ${details.renderer}`,
|
|
3420
|
+
`Tags: ${details.tags.join(", ")}`,
|
|
3421
|
+
`Variable defaults: ${Object.keys(variables.defaults).length}`,
|
|
3422
|
+
`Variable schema: ${Object.keys(variables.schema).length}`,
|
|
3423
|
+
"",
|
|
3424
|
+
"Defaults:",
|
|
3425
|
+
JSON.stringify(variables.defaults, null, 2),
|
|
3426
|
+
"",
|
|
3427
|
+
"Schema:",
|
|
3428
|
+
JSON.stringify(variables.schema, null, 2),
|
|
3429
|
+
""
|
|
3430
|
+
].join("\n"));
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
// src/validate.ts
|
|
3435
|
+
import path27 from "path";
|
|
3436
|
+
async function runValidateCommand(options = {}, io = { write: (message) => process.stdout.write(message) }) {
|
|
3437
|
+
const report = options.templateDir ? await validateTemplate({ templateDir: path27.resolve(options.cwd ?? process.cwd(), options.templateDir) }) : await validateProject({ rootDir: path27.resolve(options.cwd ?? process.cwd()) });
|
|
3438
|
+
if (options.json) {
|
|
3439
|
+
io.write(`${JSON.stringify(report, null, 2)}
|
|
3440
|
+
`);
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
io.write(`AppInit Validation: ${report.rootDir}
|
|
3444
|
+
|
|
3445
|
+
`);
|
|
3446
|
+
if (!report.diagnostics.length) io.write(" \u2713 No validation issues found.\n");
|
|
3447
|
+
for (const item of report.diagnostics) {
|
|
3448
|
+
const marker = item.level === "info" ? "i" : item.level === "warn" ? "!" : "\u2717";
|
|
3449
|
+
io.write(` ${marker} [${item.level.toUpperCase()}] ${item.code}: ${item.message}
|
|
3450
|
+
`);
|
|
3451
|
+
if (item.filePath) io.write(` File: ${item.filePath}
|
|
3452
|
+
`);
|
|
3453
|
+
if (item.hint) io.write(` Hint: ${item.hint}
|
|
3454
|
+
`);
|
|
3455
|
+
}
|
|
3456
|
+
io.write(`
|
|
3457
|
+
Result: ${report.ok ? "VALID" : "INVALID"}
|
|
3458
|
+
`);
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
// src/wizard.ts
|
|
3462
|
+
import path28 from "path";
|
|
3463
|
+
|
|
3464
|
+
// src/prompts.ts
|
|
3465
|
+
async function textPrompt(io, message, fallback) {
|
|
3466
|
+
const answer = (await io.ask(message, fallback)).trim();
|
|
3467
|
+
return answer || fallback || "";
|
|
3468
|
+
}
|
|
3469
|
+
async function confirmPrompt(io, message, fallback = false) {
|
|
3470
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
3471
|
+
const raw = (await io.ask(`${message} (yes/no)`, fallback ? "yes" : "no")).trim().toLowerCase();
|
|
3472
|
+
if (!raw) return fallback;
|
|
3473
|
+
if (["y", "yes", "true"].includes(raw)) return true;
|
|
3474
|
+
if (["n", "no", "false"].includes(raw)) return false;
|
|
3475
|
+
io.write("Please answer yes or no.\n");
|
|
3476
|
+
}
|
|
3477
|
+
throw new Error(`Invalid confirmation response for "${message}".`);
|
|
3478
|
+
}
|
|
3479
|
+
async function selectPrompt(io, message, choices, fallback) {
|
|
3480
|
+
if (!choices.length) throw new Error(`Prompt "${message}" has no choices.`);
|
|
3481
|
+
io.write(`${message} choices: ${choices.map((value, index) => `${index + 1}) ${String(value)}`).join(" ")}
|
|
3482
|
+
`);
|
|
3483
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
3484
|
+
const raw = (await io.ask(message, fallback === void 0 ? void 0 : String(fallback))).trim();
|
|
3485
|
+
if (!raw && fallback !== void 0) return fallback;
|
|
3486
|
+
const index = Number(raw) - 1;
|
|
3487
|
+
if (Number.isInteger(index) && index >= 0 && index < choices.length) return choices[index];
|
|
3488
|
+
const exact = choices.find((choice) => String(choice) === raw);
|
|
3489
|
+
if (exact !== void 0) return exact;
|
|
3490
|
+
io.write("Choose one of the listed values.\n");
|
|
3491
|
+
}
|
|
3492
|
+
throw new Error(`Invalid selection for "${message}".`);
|
|
3493
|
+
}
|
|
3494
|
+
async function schemaPrompt(io, key, rule, fallback) {
|
|
3495
|
+
if (rule.enum?.length) return selectPrompt(io, key, rule.enum, fallback);
|
|
3496
|
+
if (rule.type === "boolean") return confirmPrompt(io, key, fallback === true || fallback === "true");
|
|
3497
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
3498
|
+
const isJson = rule.type === "object" || rule.type === "array";
|
|
3499
|
+
const raw = await textPrompt(
|
|
3500
|
+
io,
|
|
3501
|
+
isJson ? `${key} (JSON)` : key,
|
|
3502
|
+
fallback === void 0 ? void 0 : isJson ? JSON.stringify(fallback) : String(fallback)
|
|
3503
|
+
);
|
|
3504
|
+
if (!raw && fallback === void 0) return raw;
|
|
3505
|
+
if (rule.type === "number") {
|
|
3506
|
+
const number = Number(raw);
|
|
3507
|
+
if (Number.isFinite(number)) return number;
|
|
3508
|
+
io.write(`"${key}" must be a valid number.
|
|
3509
|
+
`);
|
|
3510
|
+
continue;
|
|
3511
|
+
}
|
|
3512
|
+
if (isJson) {
|
|
3513
|
+
try {
|
|
3514
|
+
const value = JSON.parse(raw);
|
|
3515
|
+
if (rule.type === "array" ? Array.isArray(value) : value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
3516
|
+
return value;
|
|
3517
|
+
}
|
|
3518
|
+
} catch {
|
|
3519
|
+
}
|
|
3520
|
+
io.write(`"${key}" must be valid ${rule.type} JSON.
|
|
3521
|
+
`);
|
|
3522
|
+
continue;
|
|
3523
|
+
}
|
|
3524
|
+
return raw;
|
|
3525
|
+
}
|
|
3526
|
+
throw new Error(`Invalid value for template variable "${key}".`);
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
// src/wizard.ts
|
|
3530
|
+
async function resolveGuidedCreate(options, io) {
|
|
3531
|
+
const templateDirectory = resolveTemplateDirectory(options.templateDirectory);
|
|
3532
|
+
const templates = await discoverLocalTemplates(templateDirectory);
|
|
3533
|
+
if (!templates.length) throw new Error(`No valid local templates found in ${templateDirectory}.`);
|
|
3534
|
+
let selected = options.template ? templates.find((item) => item.id === options.template || path28.basename(item.root) === options.template || item.template.manifest.packageName === options.template) : void 0;
|
|
3535
|
+
if (options.template && !selected) {
|
|
3536
|
+
throw new Error(`Template "${options.template}" was not found in ${templateDirectory}.`);
|
|
3537
|
+
}
|
|
3538
|
+
if (!selected) {
|
|
3539
|
+
io.write("Available templates:\n");
|
|
3540
|
+
templates.forEach((item, index2) => io.write(` ${index2 + 1}. ${item.name} \u2014 ${item.description}
|
|
3541
|
+
`));
|
|
3542
|
+
const answer = await textPrompt(io, "Select a template number");
|
|
3543
|
+
const index = Number(answer) - 1;
|
|
3544
|
+
if (!Number.isInteger(index) || index < 0 || index >= templates.length) {
|
|
3545
|
+
throw new Error("Invalid template selection.");
|
|
3546
|
+
}
|
|
3547
|
+
selected = templates[index];
|
|
3548
|
+
}
|
|
3549
|
+
let target = options.target;
|
|
3550
|
+
if (!target) target = await textPrompt(io, "Project name", "my-app") || "my-app";
|
|
3551
|
+
const targetName = path28.basename(path28.resolve(target));
|
|
3552
|
+
const metadata = await loadTemplateVariables(selected.root, selected.template.manifest);
|
|
3553
|
+
const variables = { ...metadata.defaults, ...options.variables };
|
|
3554
|
+
if (Object.hasOwn(metadata.schema, "projectName")) {
|
|
3555
|
+
variables.projectName = options.variables.projectName ?? targetName;
|
|
3556
|
+
}
|
|
3557
|
+
for (const [key, rule] of Object.entries(metadata.schema)) {
|
|
3558
|
+
if (Object.hasOwn(options.variables, key) || key === "projectName") continue;
|
|
3559
|
+
if (options.yes || !io.interactive) continue;
|
|
3560
|
+
const answer = await schemaPrompt(io, key, rule, metadata.defaults[key]);
|
|
3561
|
+
if (answer !== "") variables[key] = answer;
|
|
3562
|
+
}
|
|
3563
|
+
const errors = validateTemplateVariables2(variables, metadata.schema);
|
|
3564
|
+
if (errors.length) {
|
|
3565
|
+
const details = errors.map((item) => ` - ${item}`).join("\n");
|
|
3566
|
+
throw new Error(`Template variables are incomplete or invalid:
|
|
3567
|
+
${details}${options.yes ? "\nProvide missing values with --var key=value." : ""}`);
|
|
3568
|
+
}
|
|
3569
|
+
io.write("\nProject configuration\n");
|
|
3570
|
+
io.write(` Template: ${selected.name}
|
|
3571
|
+
Target: ${path28.resolve(target)}
|
|
3572
|
+
`);
|
|
3573
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
3574
|
+
io.write(` ${key}: ${typeof value === "object" ? JSON.stringify(value) : String(value)}
|
|
3575
|
+
`);
|
|
3576
|
+
}
|
|
3577
|
+
if (!options.yes && !options.dryRun && !await io.confirm("Create this project?")) {
|
|
3578
|
+
io.write("Cancelled. No files were written.\n");
|
|
3579
|
+
return null;
|
|
3580
|
+
}
|
|
3581
|
+
return Object.freeze({ ...options, template: selected.id, target, variables });
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
// src/index.ts
|
|
3585
|
+
var CREATE_HELP = [
|
|
3586
|
+
"Usage: appinit create [project-name] [options]",
|
|
3587
|
+
" appinit create <template-ref> <target> [options]",
|
|
3588
|
+
"",
|
|
3589
|
+
"Create a project from a local AppInit template. Without a template,",
|
|
3590
|
+
"the interactive wizard helps you choose one and configure its variables.",
|
|
3591
|
+
"",
|
|
3592
|
+
"Options:",
|
|
3593
|
+
" -t, --template <ref> Select a template by directory, ID, or package name",
|
|
3594
|
+
" --var key=value Set a template variable (repeatable)",
|
|
3595
|
+
" --template-dir <path> Use a specific local template directory",
|
|
3596
|
+
" -y, --yes Skip confirmation (requires all choices/values)",
|
|
3597
|
+
" --dry-run Preview planned output without writing files",
|
|
3598
|
+
" --install Explicitly install dependencies (headless opt-in)",
|
|
3599
|
+
" --package-manager <pm> Choose pnpm, npm, yarn, or bun",
|
|
3600
|
+
" --git Explicitly initialize a Git repository",
|
|
3601
|
+
" -h, --help Show this help",
|
|
3602
|
+
"",
|
|
3603
|
+
"Examples:",
|
|
3604
|
+
" appinit create",
|
|
3605
|
+
" appinit create my-api",
|
|
3606
|
+
" appinit create my-api --template backend.fastify.basic",
|
|
3607
|
+
" appinit create backend.backend-express.basic ./my-api --dry-run",
|
|
3608
|
+
" appinit create my-api --template backend.fastify.basic --yes",
|
|
3609
|
+
"",
|
|
3610
|
+
"Template directory defaults to ./templates or APPINIT_TEMPLATES_DIR.",
|
|
3611
|
+
""
|
|
3612
|
+
].join("\n");
|
|
3613
|
+
async function main(argv) {
|
|
3614
|
+
const [command, ...args] = argv;
|
|
3615
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
3616
|
+
process.stdout.write([
|
|
3617
|
+
"AppInit CLI",
|
|
3618
|
+
"",
|
|
3619
|
+
"Commands:",
|
|
3620
|
+
" appinit create [project-name] Create a project with guided setup",
|
|
3621
|
+
" appinit template <command> Author, discover, and install templates",
|
|
3622
|
+
" appinit doctor [--json] Diagnose project health",
|
|
3623
|
+
" appinit history [--json] View previously generated projects",
|
|
3624
|
+
" appinit context [--json] Inspect current project context",
|
|
3625
|
+
" appinit state path Show the global state directory path",
|
|
3626
|
+
" appinit state inspect [--json] Inspect global state safely",
|
|
3627
|
+
" appinit state doctor [--json] Diagnose global state health",
|
|
3628
|
+
" appinit validate [options] Validate a project or template",
|
|
3629
|
+
"",
|
|
3630
|
+
'Run "appinit create --help", "appinit template --help", "appinit doctor --help", "appinit history --help", "appinit context --help", "appinit state path --help", "appinit state inspect --help", or "appinit validate --help" for usage.',
|
|
3631
|
+
""
|
|
3632
|
+
].join("\n"));
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3635
|
+
if (command === "context") {
|
|
3636
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3637
|
+
stdout.write("Usage: appinit context [--json]\n\nShow metadata and global history for the current project.\n");
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3640
|
+
const unknown = args.filter((arg) => arg !== "--json");
|
|
3641
|
+
if (unknown.length) throw new Error("Unknown context option: " + unknown[0]);
|
|
3642
|
+
await runContextCommand({ json: args.includes("--json") });
|
|
3643
|
+
return;
|
|
3644
|
+
}
|
|
3645
|
+
if (command === "state") {
|
|
3646
|
+
const [subcommand, ...stateArgs] = args;
|
|
3647
|
+
if (subcommand === "doctor") {
|
|
3648
|
+
if (stateArgs.includes("--help") || stateArgs.includes("-h")) {
|
|
3649
|
+
stdout.write("Usage: appinit state doctor [--json]\n\nDiagnose global state health without modifying it.\n");
|
|
3650
|
+
return;
|
|
3651
|
+
}
|
|
3652
|
+
const unknown = stateArgs.filter((arg) => arg !== "--json");
|
|
3653
|
+
if (unknown.length) throw new Error(`Unknown state doctor option "${unknown[0]}".`);
|
|
3654
|
+
await runStateDoctorCommand({ json: stateArgs.includes("--json") });
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
if (subcommand === "inspect") {
|
|
3658
|
+
if (stateArgs.includes("--help") || stateArgs.includes("-h")) {
|
|
3659
|
+
stdout.write("Usage: appinit state inspect [--json]\n\nInspect global state without modifying it.\n");
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
const unknown = stateArgs.filter((arg) => arg !== "--json");
|
|
3663
|
+
if (unknown.length) throw new Error(`Unknown state inspect option "${unknown[0]}".`);
|
|
3664
|
+
await runStateInspectCommand({ json: stateArgs.includes("--json") });
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
if (subcommand !== "path") {
|
|
3668
|
+
throw new Error('Unknown state command. Use "appinit state path", "appinit state inspect", or "appinit state doctor".');
|
|
3669
|
+
}
|
|
3670
|
+
if (stateArgs.includes("--help") || stateArgs.includes("-h")) {
|
|
3671
|
+
stdout.write("Usage: appinit state path\n\nShow the resolved global AppInit state directory.\n");
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
if (stateArgs.length) throw new Error(`Unknown state path option "${stateArgs[0]}".`);
|
|
3675
|
+
await runStatePathCommand();
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
if (command === "history") {
|
|
3679
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3680
|
+
stdout.write("Usage: appinit history [--json]\n\nShow projects previously generated with AppInit.\n");
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
3683
|
+
const unknown = args.filter((arg) => arg !== "--json");
|
|
3684
|
+
if (unknown.length) throw new Error(`Unknown history option "${unknown[0]}".`);
|
|
3685
|
+
await runHistoryCommand({ json: args.includes("--json") });
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
if (command === "doctor") {
|
|
3689
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3690
|
+
stdout.write("Usage: appinit doctor [--json]\n\nDiagnose the current project. Doctor findings are advisory and do not block execution.\n");
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
const unknown = args.filter((arg) => arg !== "--json");
|
|
3694
|
+
if (unknown.length) throw new Error(`Unknown doctor option "${unknown[0]}".`);
|
|
3695
|
+
await runDoctorCommand({ json: args.includes("--json") });
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
if (command === "validate") {
|
|
3699
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3700
|
+
stdout.write("Usage: appinit validate [--template <directory>] [--json]\n\nValidate the current project or a template directory.\n");
|
|
3701
|
+
return;
|
|
3702
|
+
}
|
|
3703
|
+
let templateDir;
|
|
3704
|
+
let json = false;
|
|
3705
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3706
|
+
const arg = args[index];
|
|
3707
|
+
if (arg === "--json") json = true;
|
|
3708
|
+
else if (arg === "--template") {
|
|
3709
|
+
templateDir = args[index + 1];
|
|
3710
|
+
if (!templateDir || templateDir.startsWith("--")) throw new Error("--template requires a directory.");
|
|
3711
|
+
index += 1;
|
|
3712
|
+
} else throw new Error(`Unknown validate option "${arg}".`);
|
|
3713
|
+
}
|
|
3714
|
+
await runValidateCommand({ templateDir, json });
|
|
3715
|
+
return;
|
|
3716
|
+
}
|
|
3717
|
+
if (command === "template") {
|
|
3718
|
+
await runTemplateCommand(args, { write: (message) => stdout.write(message) });
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
if (command !== "create") {
|
|
3722
|
+
throw new Error(`Unknown command "${command}". Run "appinit --help" for usage.`);
|
|
3723
|
+
}
|
|
3724
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
3725
|
+
process.stdout.write(CREATE_HELP);
|
|
3726
|
+
return;
|
|
3727
|
+
}
|
|
3728
|
+
const options = parseCreateArguments(args);
|
|
3729
|
+
const interactive = Boolean(stdin.isTTY && stdout.isTTY);
|
|
3730
|
+
if (!interactive && (!options.template || !options.target)) {
|
|
3731
|
+
throw new Error("Non-interactive create requires a template and target. Use: appinit create <template-ref> <target> --yes");
|
|
3732
|
+
}
|
|
3733
|
+
if (!interactive && !options.yes && !options.dryRun) {
|
|
3734
|
+
throw new Error("Non-interactive create requires --yes to confirm file writes.");
|
|
3735
|
+
}
|
|
3736
|
+
if (options.yes && (!options.template || !options.target)) {
|
|
3737
|
+
throw new Error(" --yes requires both a template and target. Supply: appinit create <template-ref> <target> --yes");
|
|
3738
|
+
}
|
|
3739
|
+
const rl = interactive ? createInterface({ input: stdin, output: stdout }) : void 0;
|
|
3740
|
+
const io = {
|
|
3741
|
+
interactive,
|
|
3742
|
+
ask: async (message, fallback) => {
|
|
3743
|
+
if (!rl) throw new Error(`Missing value: ${message}`);
|
|
3744
|
+
const suffix = fallback ? ` [${fallback}]` : "";
|
|
3745
|
+
const answer = (await rl.question(`${message}${suffix}: `)).trim();
|
|
3746
|
+
return answer || fallback || "";
|
|
3747
|
+
},
|
|
3748
|
+
confirm: async (message) => {
|
|
3749
|
+
if (!rl) throw new Error("Confirmation requires an interactive terminal. Use --yes to proceed.");
|
|
3750
|
+
const answer = (await rl.question(`${message} (y/N): `)).trim().toLowerCase();
|
|
3751
|
+
return answer === "y" || answer === "yes";
|
|
3752
|
+
},
|
|
3753
|
+
write: (message) => {
|
|
3754
|
+
stdout.write(message);
|
|
3755
|
+
}
|
|
3756
|
+
};
|
|
3757
|
+
try {
|
|
3758
|
+
const resolved = await resolveGuidedCreate(options, io);
|
|
3759
|
+
if (resolved) {
|
|
3760
|
+
await runCreateCommand(resolved);
|
|
3761
|
+
if (!resolved.dryRun) await finalizeProject(resolved.target, resolved, io);
|
|
3762
|
+
}
|
|
3763
|
+
} finally {
|
|
3764
|
+
rl?.close();
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
3768
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3769
|
+
process.stderr.write(`AppInit: ${message}
|
|
3770
|
+
`);
|
|
3771
|
+
process.exitCode = 1;
|
|
3772
|
+
});
|
|
3773
|
+
export {
|
|
3774
|
+
diagnoseAppInitState,
|
|
3775
|
+
finalizeProject,
|
|
3776
|
+
findLocalTemplate,
|
|
3777
|
+
formatTemplateInfo,
|
|
3778
|
+
formatTemplateList,
|
|
3779
|
+
inspectAppInitState,
|
|
3780
|
+
inspectAppInitStatePath,
|
|
3781
|
+
installRemoteTemplate,
|
|
3782
|
+
listRemoteTemplates,
|
|
3783
|
+
parseCreateArguments,
|
|
3784
|
+
readProjectContext,
|
|
3785
|
+
readProjectHistory,
|
|
3786
|
+
resolveGuidedCreate,
|
|
3787
|
+
runContextCommand,
|
|
3788
|
+
runCreateCommand,
|
|
3789
|
+
runDoctorCommand,
|
|
3790
|
+
runHistoryCommand,
|
|
3791
|
+
runStateDoctorCommand,
|
|
3792
|
+
runStateInspectCommand,
|
|
3793
|
+
runStatePathCommand,
|
|
3794
|
+
runValidateCommand,
|
|
3795
|
+
searchLocalTemplates
|
|
3796
|
+
};
|