@linguanahq/cli 0.1.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 +21 -0
- package/README.md +43 -0
- package/dist/index.js +1543 -0
- package/dist/templates/blog/react/.env.example +15 -0
- package/dist/templates/blog/react/ASSETS.md +1 -0
- package/dist/templates/blog/react/DEPLOY.md +23 -0
- package/dist/templates/blog/react/LICENSE +21 -0
- package/dist/templates/blog/react/README.md +36 -0
- package/dist/templates/blog/react/_gitignore +11 -0
- package/dist/templates/blog/react/index.html +2 -0
- package/dist/templates/blog/react/linguana.json +10 -0
- package/dist/templates/blog/react/package.json +34 -0
- package/dist/templates/blog/react/public/assets/scene-1.svg +1 -0
- package/dist/templates/blog/react/public/assets/scene-2.svg +1 -0
- package/dist/templates/blog/react/public/assets/scene-3.svg +1 -0
- package/dist/templates/blog/react/public/assets/scene-4.svg +1 -0
- package/dist/templates/blog/react/public/assets/scene-5.svg +1 -0
- package/dist/templates/blog/react/public/assets/scene-6.svg +1 -0
- package/dist/templates/blog/react/src/App.tsx +465 -0
- package/dist/templates/blog/react/src/Copy.tsx +322 -0
- package/dist/templates/blog/react/src/demo-messages.ts +221 -0
- package/dist/templates/blog/react/src/i18n.ts +62 -0
- package/dist/templates/blog/react/src/main.tsx +11 -0
- package/dist/templates/blog/react/src/messages.ts +71 -0
- package/dist/templates/blog/react/src/model.ts +128 -0
- package/dist/templates/blog/react/src/styles.css +1159 -0
- package/dist/templates/blog/react/src/vite-env.d.ts +1 -0
- package/dist/templates/blog/react/tsconfig.json +23 -0
- package/dist/templates/blog/react/vite.config.ts +36 -0
- package/dist/templates/blog/react/wrangler.jsonc +8 -0
- package/dist/templates/blog/solidjs/.env.example +15 -0
- package/dist/templates/blog/solidjs/ASSETS.md +1 -0
- package/dist/templates/blog/solidjs/DEPLOY.md +23 -0
- package/dist/templates/blog/solidjs/LICENSE +21 -0
- package/dist/templates/blog/solidjs/README.md +36 -0
- package/dist/templates/blog/solidjs/_gitignore +11 -0
- package/dist/templates/blog/solidjs/index.html +2 -0
- package/dist/templates/blog/solidjs/linguana.json +10 -0
- package/dist/templates/blog/solidjs/package.json +31 -0
- package/dist/templates/blog/solidjs/public/assets/scene-1.svg +1 -0
- package/dist/templates/blog/solidjs/public/assets/scene-2.svg +1 -0
- package/dist/templates/blog/solidjs/public/assets/scene-3.svg +1 -0
- package/dist/templates/blog/solidjs/public/assets/scene-4.svg +1 -0
- package/dist/templates/blog/solidjs/public/assets/scene-5.svg +1 -0
- package/dist/templates/blog/solidjs/public/assets/scene-6.svg +1 -0
- package/dist/templates/blog/solidjs/src/App.tsx +457 -0
- package/dist/templates/blog/solidjs/src/Copy.tsx +322 -0
- package/dist/templates/blog/solidjs/src/demo-messages.ts +221 -0
- package/dist/templates/blog/solidjs/src/i18n.ts +62 -0
- package/dist/templates/blog/solidjs/src/main.tsx +14 -0
- package/dist/templates/blog/solidjs/src/messages.ts +71 -0
- package/dist/templates/blog/solidjs/src/model.ts +128 -0
- package/dist/templates/blog/solidjs/src/styles.css +1159 -0
- package/dist/templates/blog/solidjs/src/vite-env.d.ts +1 -0
- package/dist/templates/blog/solidjs/tsconfig.json +24 -0
- package/dist/templates/blog/solidjs/vite.config.ts +36 -0
- package/dist/templates/blog/solidjs/wrangler.jsonc +8 -0
- package/dist/templates/blog/svelte/.env.example +15 -0
- package/dist/templates/blog/svelte/ASSETS.md +1 -0
- package/dist/templates/blog/svelte/DEPLOY.md +23 -0
- package/dist/templates/blog/svelte/LICENSE +21 -0
- package/dist/templates/blog/svelte/README.md +36 -0
- package/dist/templates/blog/svelte/_gitignore +11 -0
- package/dist/templates/blog/svelte/index.html +2 -0
- package/dist/templates/blog/svelte/linguana.json +10 -0
- package/dist/templates/blog/svelte/package.json +32 -0
- package/dist/templates/blog/svelte/public/assets/scene-1.svg +1 -0
- package/dist/templates/blog/svelte/public/assets/scene-2.svg +1 -0
- package/dist/templates/blog/svelte/public/assets/scene-3.svg +1 -0
- package/dist/templates/blog/svelte/public/assets/scene-4.svg +1 -0
- package/dist/templates/blog/svelte/public/assets/scene-5.svg +1 -0
- package/dist/templates/blog/svelte/public/assets/scene-6.svg +1 -0
- package/dist/templates/blog/svelte/src/App.svelte +196 -0
- package/dist/templates/blog/svelte/src/Copy.svelte +60 -0
- package/dist/templates/blog/svelte/src/Root.svelte +11 -0
- package/dist/templates/blog/svelte/src/demo-messages.ts +221 -0
- package/dist/templates/blog/svelte/src/i18n.ts +62 -0
- package/dist/templates/blog/svelte/src/main.ts +5 -0
- package/dist/templates/blog/svelte/src/messages.ts +71 -0
- package/dist/templates/blog/svelte/src/model.ts +128 -0
- package/dist/templates/blog/svelte/src/styles.css +1159 -0
- package/dist/templates/blog/svelte/src/vite-env.d.ts +1 -0
- package/dist/templates/blog/svelte/svelte.config.js +2 -0
- package/dist/templates/blog/svelte/tsconfig.json +23 -0
- package/dist/templates/blog/svelte/vite.config.ts +36 -0
- package/dist/templates/blog/svelte/wrangler.jsonc +8 -0
- package/dist/templates/blog/tanstack-start/.env.example +15 -0
- package/dist/templates/blog/tanstack-start/ASSETS.md +1 -0
- package/dist/templates/blog/tanstack-start/DEPLOY.md +23 -0
- package/dist/templates/blog/tanstack-start/LICENSE +21 -0
- package/dist/templates/blog/tanstack-start/README.md +36 -0
- package/dist/templates/blog/tanstack-start/_gitignore +11 -0
- package/dist/templates/blog/tanstack-start/linguana.json +10 -0
- package/dist/templates/blog/tanstack-start/nitro.config.ts +2 -0
- package/dist/templates/blog/tanstack-start/package.json +42 -0
- package/dist/templates/blog/tanstack-start/public/assets/scene-1.svg +1 -0
- package/dist/templates/blog/tanstack-start/public/assets/scene-2.svg +1 -0
- package/dist/templates/blog/tanstack-start/public/assets/scene-3.svg +1 -0
- package/dist/templates/blog/tanstack-start/public/assets/scene-4.svg +1 -0
- package/dist/templates/blog/tanstack-start/public/assets/scene-5.svg +1 -0
- package/dist/templates/blog/tanstack-start/public/assets/scene-6.svg +1 -0
- package/dist/templates/blog/tanstack-start/src/App.tsx +450 -0
- package/dist/templates/blog/tanstack-start/src/Copy.tsx +322 -0
- package/dist/templates/blog/tanstack-start/src/demo-messages.ts +221 -0
- package/dist/templates/blog/tanstack-start/src/i18n.ts +62 -0
- package/dist/templates/blog/tanstack-start/src/locale-server.ts +54 -0
- package/dist/templates/blog/tanstack-start/src/messages.ts +71 -0
- package/dist/templates/blog/tanstack-start/src/model.ts +128 -0
- package/dist/templates/blog/tanstack-start/src/router.tsx +10 -0
- package/dist/templates/blog/tanstack-start/src/routes/$.tsx +7 -0
- package/dist/templates/blog/tanstack-start/src/routes/__root.tsx +50 -0
- package/dist/templates/blog/tanstack-start/src/routes/index.tsx +5 -0
- package/dist/templates/blog/tanstack-start/src/styles.css +1159 -0
- package/dist/templates/blog/tanstack-start/src/vite-env.d.ts +1 -0
- package/dist/templates/blog/tanstack-start/tsconfig.json +23 -0
- package/dist/templates/blog/tanstack-start/vite.config.ts +50 -0
- package/dist/templates/blog/tanstack-start/wrangler.jsonc +12 -0
- package/dist/templates/blog/vue/.env.example +15 -0
- package/dist/templates/blog/vue/ASSETS.md +1 -0
- package/dist/templates/blog/vue/DEPLOY.md +23 -0
- package/dist/templates/blog/vue/LICENSE +21 -0
- package/dist/templates/blog/vue/README.md +36 -0
- package/dist/templates/blog/vue/_gitignore +11 -0
- package/dist/templates/blog/vue/index.html +2 -0
- package/dist/templates/blog/vue/linguana.json +10 -0
- package/dist/templates/blog/vue/package.json +34 -0
- package/dist/templates/blog/vue/public/assets/scene-1.svg +1 -0
- package/dist/templates/blog/vue/public/assets/scene-2.svg +1 -0
- package/dist/templates/blog/vue/public/assets/scene-3.svg +1 -0
- package/dist/templates/blog/vue/public/assets/scene-4.svg +1 -0
- package/dist/templates/blog/vue/public/assets/scene-5.svg +1 -0
- package/dist/templates/blog/vue/public/assets/scene-6.svg +1 -0
- package/dist/templates/blog/vue/src/App.vue +188 -0
- package/dist/templates/blog/vue/src/Copy.vue +59 -0
- package/dist/templates/blog/vue/src/demo-messages.ts +221 -0
- package/dist/templates/blog/vue/src/i18n.ts +62 -0
- package/dist/templates/blog/vue/src/main.ts +7 -0
- package/dist/templates/blog/vue/src/messages.ts +71 -0
- package/dist/templates/blog/vue/src/model.ts +128 -0
- package/dist/templates/blog/vue/src/styles.css +1159 -0
- package/dist/templates/blog/vue/src/vite-env.d.ts +1 -0
- package/dist/templates/blog/vue/tsconfig.json +23 -0
- package/dist/templates/blog/vue/vite.config.ts +36 -0
- package/dist/templates/blog/vue/wrangler.jsonc +8 -0
- package/dist/templates/saas/react/.env.example +15 -0
- package/dist/templates/saas/react/ASSETS.md +1 -0
- package/dist/templates/saas/react/DEPLOY.md +23 -0
- package/dist/templates/saas/react/LICENSE +21 -0
- package/dist/templates/saas/react/README.md +36 -0
- package/dist/templates/saas/react/_gitignore +11 -0
- package/dist/templates/saas/react/index.html +2 -0
- package/dist/templates/saas/react/linguana.json +10 -0
- package/dist/templates/saas/react/package.json +34 -0
- package/dist/templates/saas/react/src/App.tsx +631 -0
- package/dist/templates/saas/react/src/Copy.tsx +380 -0
- package/dist/templates/saas/react/src/demo-messages.ts +228 -0
- package/dist/templates/saas/react/src/i18n.ts +62 -0
- package/dist/templates/saas/react/src/main.tsx +11 -0
- package/dist/templates/saas/react/src/messages.ts +77 -0
- package/dist/templates/saas/react/src/model.ts +73 -0
- package/dist/templates/saas/react/src/styles.css +1159 -0
- package/dist/templates/saas/react/src/vite-env.d.ts +1 -0
- package/dist/templates/saas/react/tsconfig.json +23 -0
- package/dist/templates/saas/react/vite.config.ts +36 -0
- package/dist/templates/saas/react/wrangler.jsonc +8 -0
- package/dist/templates/saas/solidjs/.env.example +15 -0
- package/dist/templates/saas/solidjs/ASSETS.md +1 -0
- package/dist/templates/saas/solidjs/DEPLOY.md +23 -0
- package/dist/templates/saas/solidjs/LICENSE +21 -0
- package/dist/templates/saas/solidjs/README.md +36 -0
- package/dist/templates/saas/solidjs/_gitignore +11 -0
- package/dist/templates/saas/solidjs/index.html +2 -0
- package/dist/templates/saas/solidjs/linguana.json +10 -0
- package/dist/templates/saas/solidjs/package.json +31 -0
- package/dist/templates/saas/solidjs/src/App.tsx +629 -0
- package/dist/templates/saas/solidjs/src/Copy.tsx +380 -0
- package/dist/templates/saas/solidjs/src/demo-messages.ts +228 -0
- package/dist/templates/saas/solidjs/src/i18n.ts +62 -0
- package/dist/templates/saas/solidjs/src/main.tsx +14 -0
- package/dist/templates/saas/solidjs/src/messages.ts +77 -0
- package/dist/templates/saas/solidjs/src/model.ts +73 -0
- package/dist/templates/saas/solidjs/src/styles.css +1159 -0
- package/dist/templates/saas/solidjs/src/vite-env.d.ts +1 -0
- package/dist/templates/saas/solidjs/tsconfig.json +24 -0
- package/dist/templates/saas/solidjs/vite.config.ts +36 -0
- package/dist/templates/saas/solidjs/wrangler.jsonc +8 -0
- package/dist/templates/saas/svelte/.env.example +15 -0
- package/dist/templates/saas/svelte/ASSETS.md +1 -0
- package/dist/templates/saas/svelte/DEPLOY.md +23 -0
- package/dist/templates/saas/svelte/LICENSE +21 -0
- package/dist/templates/saas/svelte/README.md +36 -0
- package/dist/templates/saas/svelte/_gitignore +11 -0
- package/dist/templates/saas/svelte/index.html +2 -0
- package/dist/templates/saas/svelte/linguana.json +10 -0
- package/dist/templates/saas/svelte/package.json +32 -0
- package/dist/templates/saas/svelte/src/App.svelte +252 -0
- package/dist/templates/saas/svelte/src/Copy.svelte +74 -0
- package/dist/templates/saas/svelte/src/Root.svelte +11 -0
- package/dist/templates/saas/svelte/src/demo-messages.ts +228 -0
- package/dist/templates/saas/svelte/src/i18n.ts +62 -0
- package/dist/templates/saas/svelte/src/main.ts +5 -0
- package/dist/templates/saas/svelte/src/messages.ts +77 -0
- package/dist/templates/saas/svelte/src/model.ts +73 -0
- package/dist/templates/saas/svelte/src/styles.css +1159 -0
- package/dist/templates/saas/svelte/src/vite-env.d.ts +1 -0
- package/dist/templates/saas/svelte/svelte.config.js +2 -0
- package/dist/templates/saas/svelte/tsconfig.json +23 -0
- package/dist/templates/saas/svelte/vite.config.ts +36 -0
- package/dist/templates/saas/svelte/wrangler.jsonc +8 -0
- package/dist/templates/saas/tanstack-start/.env.example +15 -0
- package/dist/templates/saas/tanstack-start/ASSETS.md +1 -0
- package/dist/templates/saas/tanstack-start/DEPLOY.md +23 -0
- package/dist/templates/saas/tanstack-start/LICENSE +21 -0
- package/dist/templates/saas/tanstack-start/README.md +36 -0
- package/dist/templates/saas/tanstack-start/_gitignore +11 -0
- package/dist/templates/saas/tanstack-start/linguana.json +10 -0
- package/dist/templates/saas/tanstack-start/nitro.config.ts +2 -0
- package/dist/templates/saas/tanstack-start/package.json +42 -0
- package/dist/templates/saas/tanstack-start/src/App.tsx +616 -0
- package/dist/templates/saas/tanstack-start/src/Copy.tsx +380 -0
- package/dist/templates/saas/tanstack-start/src/demo-messages.ts +228 -0
- package/dist/templates/saas/tanstack-start/src/i18n.ts +62 -0
- package/dist/templates/saas/tanstack-start/src/locale-server.ts +54 -0
- package/dist/templates/saas/tanstack-start/src/messages.ts +77 -0
- package/dist/templates/saas/tanstack-start/src/model.ts +73 -0
- package/dist/templates/saas/tanstack-start/src/router.tsx +10 -0
- package/dist/templates/saas/tanstack-start/src/routes/$.tsx +7 -0
- package/dist/templates/saas/tanstack-start/src/routes/__root.tsx +50 -0
- package/dist/templates/saas/tanstack-start/src/routes/index.tsx +5 -0
- package/dist/templates/saas/tanstack-start/src/styles.css +1159 -0
- package/dist/templates/saas/tanstack-start/src/vite-env.d.ts +1 -0
- package/dist/templates/saas/tanstack-start/tsconfig.json +23 -0
- package/dist/templates/saas/tanstack-start/vite.config.ts +50 -0
- package/dist/templates/saas/tanstack-start/wrangler.jsonc +12 -0
- package/dist/templates/saas/vue/.env.example +15 -0
- package/dist/templates/saas/vue/ASSETS.md +1 -0
- package/dist/templates/saas/vue/DEPLOY.md +23 -0
- package/dist/templates/saas/vue/LICENSE +21 -0
- package/dist/templates/saas/vue/README.md +36 -0
- package/dist/templates/saas/vue/_gitignore +11 -0
- package/dist/templates/saas/vue/index.html +2 -0
- package/dist/templates/saas/vue/linguana.json +10 -0
- package/dist/templates/saas/vue/package.json +34 -0
- package/dist/templates/saas/vue/src/App.vue +244 -0
- package/dist/templates/saas/vue/src/Copy.vue +73 -0
- package/dist/templates/saas/vue/src/demo-messages.ts +228 -0
- package/dist/templates/saas/vue/src/i18n.ts +62 -0
- package/dist/templates/saas/vue/src/main.ts +7 -0
- package/dist/templates/saas/vue/src/messages.ts +77 -0
- package/dist/templates/saas/vue/src/model.ts +73 -0
- package/dist/templates/saas/vue/src/styles.css +1159 -0
- package/dist/templates/saas/vue/src/vite-env.d.ts +1 -0
- package/dist/templates/saas/vue/tsconfig.json +23 -0
- package/dist/templates/saas/vue/vite.config.ts +36 -0
- package/dist/templates/saas/vue/wrangler.jsonc +8 -0
- package/dist/templates/storefront/react/.env.example +15 -0
- package/dist/templates/storefront/react/ASSETS.md +1 -0
- package/dist/templates/storefront/react/DEPLOY.md +23 -0
- package/dist/templates/storefront/react/LICENSE +21 -0
- package/dist/templates/storefront/react/README.md +36 -0
- package/dist/templates/storefront/react/_gitignore +11 -0
- package/dist/templates/storefront/react/index.html +2 -0
- package/dist/templates/storefront/react/linguana.json +10 -0
- package/dist/templates/storefront/react/package.json +34 -0
- package/dist/templates/storefront/react/public/assets/collection.svg +1 -0
- package/dist/templates/storefront/react/public/assets/product-1.svg +1 -0
- package/dist/templates/storefront/react/public/assets/product-2.svg +1 -0
- package/dist/templates/storefront/react/public/assets/product-3.svg +1 -0
- package/dist/templates/storefront/react/public/assets/product-4.svg +1 -0
- package/dist/templates/storefront/react/public/assets/product-5.svg +1 -0
- package/dist/templates/storefront/react/public/assets/product-6.svg +1 -0
- package/dist/templates/storefront/react/public/assets/product-7.svg +1 -0
- package/dist/templates/storefront/react/public/assets/product-8.svg +1 -0
- package/dist/templates/storefront/react/src/App.tsx +616 -0
- package/dist/templates/storefront/react/src/Copy.tsx +304 -0
- package/dist/templates/storefront/react/src/demo-messages.ts +205 -0
- package/dist/templates/storefront/react/src/i18n.ts +64 -0
- package/dist/templates/storefront/react/src/main.tsx +11 -0
- package/dist/templates/storefront/react/src/messages.ts +71 -0
- package/dist/templates/storefront/react/src/model.ts +146 -0
- package/dist/templates/storefront/react/src/styles.css +1159 -0
- package/dist/templates/storefront/react/src/vite-env.d.ts +1 -0
- package/dist/templates/storefront/react/tsconfig.json +23 -0
- package/dist/templates/storefront/react/vite.config.ts +36 -0
- package/dist/templates/storefront/react/wrangler.jsonc +8 -0
- package/dist/templates/storefront/solidjs/.env.example +15 -0
- package/dist/templates/storefront/solidjs/ASSETS.md +1 -0
- package/dist/templates/storefront/solidjs/DEPLOY.md +23 -0
- package/dist/templates/storefront/solidjs/LICENSE +21 -0
- package/dist/templates/storefront/solidjs/README.md +36 -0
- package/dist/templates/storefront/solidjs/_gitignore +11 -0
- package/dist/templates/storefront/solidjs/index.html +2 -0
- package/dist/templates/storefront/solidjs/linguana.json +10 -0
- package/dist/templates/storefront/solidjs/package.json +31 -0
- package/dist/templates/storefront/solidjs/public/assets/collection.svg +1 -0
- package/dist/templates/storefront/solidjs/public/assets/product-1.svg +1 -0
- package/dist/templates/storefront/solidjs/public/assets/product-2.svg +1 -0
- package/dist/templates/storefront/solidjs/public/assets/product-3.svg +1 -0
- package/dist/templates/storefront/solidjs/public/assets/product-4.svg +1 -0
- package/dist/templates/storefront/solidjs/public/assets/product-5.svg +1 -0
- package/dist/templates/storefront/solidjs/public/assets/product-6.svg +1 -0
- package/dist/templates/storefront/solidjs/public/assets/product-7.svg +1 -0
- package/dist/templates/storefront/solidjs/public/assets/product-8.svg +1 -0
- package/dist/templates/storefront/solidjs/src/App.tsx +612 -0
- package/dist/templates/storefront/solidjs/src/Copy.tsx +304 -0
- package/dist/templates/storefront/solidjs/src/demo-messages.ts +205 -0
- package/dist/templates/storefront/solidjs/src/i18n.ts +64 -0
- package/dist/templates/storefront/solidjs/src/main.tsx +14 -0
- package/dist/templates/storefront/solidjs/src/messages.ts +71 -0
- package/dist/templates/storefront/solidjs/src/model.ts +146 -0
- package/dist/templates/storefront/solidjs/src/styles.css +1159 -0
- package/dist/templates/storefront/solidjs/src/vite-env.d.ts +1 -0
- package/dist/templates/storefront/solidjs/tsconfig.json +24 -0
- package/dist/templates/storefront/solidjs/vite.config.ts +36 -0
- package/dist/templates/storefront/solidjs/wrangler.jsonc +8 -0
- package/dist/templates/storefront/svelte/.env.example +15 -0
- package/dist/templates/storefront/svelte/ASSETS.md +1 -0
- package/dist/templates/storefront/svelte/DEPLOY.md +23 -0
- package/dist/templates/storefront/svelte/LICENSE +21 -0
- package/dist/templates/storefront/svelte/README.md +36 -0
- package/dist/templates/storefront/svelte/_gitignore +11 -0
- package/dist/templates/storefront/svelte/index.html +2 -0
- package/dist/templates/storefront/svelte/linguana.json +10 -0
- package/dist/templates/storefront/svelte/package.json +32 -0
- package/dist/templates/storefront/svelte/public/assets/collection.svg +1 -0
- package/dist/templates/storefront/svelte/public/assets/product-1.svg +1 -0
- package/dist/templates/storefront/svelte/public/assets/product-2.svg +1 -0
- package/dist/templates/storefront/svelte/public/assets/product-3.svg +1 -0
- package/dist/templates/storefront/svelte/public/assets/product-4.svg +1 -0
- package/dist/templates/storefront/svelte/public/assets/product-5.svg +1 -0
- package/dist/templates/storefront/svelte/public/assets/product-6.svg +1 -0
- package/dist/templates/storefront/svelte/public/assets/product-7.svg +1 -0
- package/dist/templates/storefront/svelte/public/assets/product-8.svg +1 -0
- package/dist/templates/storefront/svelte/src/App.svelte +218 -0
- package/dist/templates/storefront/svelte/src/Copy.svelte +66 -0
- package/dist/templates/storefront/svelte/src/Root.svelte +11 -0
- package/dist/templates/storefront/svelte/src/demo-messages.ts +205 -0
- package/dist/templates/storefront/svelte/src/i18n.ts +64 -0
- package/dist/templates/storefront/svelte/src/main.ts +5 -0
- package/dist/templates/storefront/svelte/src/messages.ts +71 -0
- package/dist/templates/storefront/svelte/src/model.ts +146 -0
- package/dist/templates/storefront/svelte/src/styles.css +1159 -0
- package/dist/templates/storefront/svelte/src/vite-env.d.ts +1 -0
- package/dist/templates/storefront/svelte/svelte.config.js +2 -0
- package/dist/templates/storefront/svelte/tsconfig.json +23 -0
- package/dist/templates/storefront/svelte/vite.config.ts +36 -0
- package/dist/templates/storefront/svelte/wrangler.jsonc +8 -0
- package/dist/templates/storefront/tanstack-start/.env.example +15 -0
- package/dist/templates/storefront/tanstack-start/ASSETS.md +1 -0
- package/dist/templates/storefront/tanstack-start/DEPLOY.md +23 -0
- package/dist/templates/storefront/tanstack-start/LICENSE +21 -0
- package/dist/templates/storefront/tanstack-start/README.md +36 -0
- package/dist/templates/storefront/tanstack-start/_gitignore +11 -0
- package/dist/templates/storefront/tanstack-start/linguana.json +10 -0
- package/dist/templates/storefront/tanstack-start/nitro.config.ts +2 -0
- package/dist/templates/storefront/tanstack-start/package.json +42 -0
- package/dist/templates/storefront/tanstack-start/public/assets/collection.svg +1 -0
- package/dist/templates/storefront/tanstack-start/public/assets/product-1.svg +1 -0
- package/dist/templates/storefront/tanstack-start/public/assets/product-2.svg +1 -0
- package/dist/templates/storefront/tanstack-start/public/assets/product-3.svg +1 -0
- package/dist/templates/storefront/tanstack-start/public/assets/product-4.svg +1 -0
- package/dist/templates/storefront/tanstack-start/public/assets/product-5.svg +1 -0
- package/dist/templates/storefront/tanstack-start/public/assets/product-6.svg +1 -0
- package/dist/templates/storefront/tanstack-start/public/assets/product-7.svg +1 -0
- package/dist/templates/storefront/tanstack-start/public/assets/product-8.svg +1 -0
- package/dist/templates/storefront/tanstack-start/src/App.tsx +601 -0
- package/dist/templates/storefront/tanstack-start/src/Copy.tsx +304 -0
- package/dist/templates/storefront/tanstack-start/src/demo-messages.ts +205 -0
- package/dist/templates/storefront/tanstack-start/src/i18n.ts +64 -0
- package/dist/templates/storefront/tanstack-start/src/locale-server.ts +54 -0
- package/dist/templates/storefront/tanstack-start/src/messages.ts +71 -0
- package/dist/templates/storefront/tanstack-start/src/model.ts +146 -0
- package/dist/templates/storefront/tanstack-start/src/router.tsx +10 -0
- package/dist/templates/storefront/tanstack-start/src/routes/$.tsx +7 -0
- package/dist/templates/storefront/tanstack-start/src/routes/__root.tsx +50 -0
- package/dist/templates/storefront/tanstack-start/src/routes/index.tsx +5 -0
- package/dist/templates/storefront/tanstack-start/src/styles.css +1159 -0
- package/dist/templates/storefront/tanstack-start/src/vite-env.d.ts +1 -0
- package/dist/templates/storefront/tanstack-start/tsconfig.json +23 -0
- package/dist/templates/storefront/tanstack-start/vite.config.ts +50 -0
- package/dist/templates/storefront/tanstack-start/wrangler.jsonc +12 -0
- package/dist/templates/storefront/vue/.env.example +15 -0
- package/dist/templates/storefront/vue/ASSETS.md +1 -0
- package/dist/templates/storefront/vue/DEPLOY.md +23 -0
- package/dist/templates/storefront/vue/LICENSE +21 -0
- package/dist/templates/storefront/vue/README.md +36 -0
- package/dist/templates/storefront/vue/_gitignore +11 -0
- package/dist/templates/storefront/vue/index.html +2 -0
- package/dist/templates/storefront/vue/linguana.json +10 -0
- package/dist/templates/storefront/vue/package.json +34 -0
- package/dist/templates/storefront/vue/public/assets/collection.svg +1 -0
- package/dist/templates/storefront/vue/public/assets/product-1.svg +1 -0
- package/dist/templates/storefront/vue/public/assets/product-2.svg +1 -0
- package/dist/templates/storefront/vue/public/assets/product-3.svg +1 -0
- package/dist/templates/storefront/vue/public/assets/product-4.svg +1 -0
- package/dist/templates/storefront/vue/public/assets/product-5.svg +1 -0
- package/dist/templates/storefront/vue/public/assets/product-6.svg +1 -0
- package/dist/templates/storefront/vue/public/assets/product-7.svg +1 -0
- package/dist/templates/storefront/vue/public/assets/product-8.svg +1 -0
- package/dist/templates/storefront/vue/src/App.vue +210 -0
- package/dist/templates/storefront/vue/src/Copy.vue +65 -0
- package/dist/templates/storefront/vue/src/demo-messages.ts +205 -0
- package/dist/templates/storefront/vue/src/i18n.ts +64 -0
- package/dist/templates/storefront/vue/src/main.ts +7 -0
- package/dist/templates/storefront/vue/src/messages.ts +71 -0
- package/dist/templates/storefront/vue/src/model.ts +146 -0
- package/dist/templates/storefront/vue/src/styles.css +1159 -0
- package/dist/templates/storefront/vue/src/vite-env.d.ts +1 -0
- package/dist/templates/storefront/vue/tsconfig.json +23 -0
- package/dist/templates/storefront/vue/vite.config.ts +36 -0
- package/dist/templates/storefront/vue/wrangler.jsonc +8 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1543 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { join as join5, resolve as resolve4 } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { group } from "@clack/prompts";
|
|
8
|
+
|
|
9
|
+
// ../templates/src/deployments.json
|
|
10
|
+
var deployments_default = {};
|
|
11
|
+
|
|
12
|
+
// ../templates/src/registry.ts
|
|
13
|
+
var templateVersion = "0.1.0";
|
|
14
|
+
var templateSourcePackageVersion = "0.1.0";
|
|
15
|
+
var sdkVersion = "0.1.1";
|
|
16
|
+
var defaultApiUrl = "https://linguana-api.mohammedibrahim.dev";
|
|
17
|
+
var frameworks = [
|
|
18
|
+
"react",
|
|
19
|
+
"vue",
|
|
20
|
+
"svelte",
|
|
21
|
+
"solidjs",
|
|
22
|
+
"tanstack-start"
|
|
23
|
+
];
|
|
24
|
+
var frameworkLabels = {
|
|
25
|
+
react: "React",
|
|
26
|
+
vue: "Vue",
|
|
27
|
+
svelte: "Svelte",
|
|
28
|
+
solidjs: "SolidJS",
|
|
29
|
+
"tanstack-start": "TanStack Start"
|
|
30
|
+
};
|
|
31
|
+
var templateIds = ["blog", "saas", "storefront"];
|
|
32
|
+
var templates = [
|
|
33
|
+
{
|
|
34
|
+
id: "blog",
|
|
35
|
+
name: "Elsewhere",
|
|
36
|
+
category: "Blog",
|
|
37
|
+
description: "A journal for the places worth taking your time in. Articles, search, and thoughtful reading layouts.",
|
|
38
|
+
color: "#244d64",
|
|
39
|
+
features: [
|
|
40
|
+
"Six complete articles",
|
|
41
|
+
"Search and categories",
|
|
42
|
+
"Localized dates"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "saas",
|
|
47
|
+
name: "Relay",
|
|
48
|
+
category: "SaaS landing page",
|
|
49
|
+
description: "A home for your next product. Interactive handovers, clear pricing, and a useful first impression.",
|
|
50
|
+
color: "#4458c5",
|
|
51
|
+
features: [
|
|
52
|
+
"Interactive product preview",
|
|
53
|
+
"Monthly and annual pricing",
|
|
54
|
+
"Accessible demo forms"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "storefront",
|
|
59
|
+
name: "Common Goods",
|
|
60
|
+
category: "Storefront",
|
|
61
|
+
description: "Everyday essentials, considered carefully. Product browsing, a persistent cart, and room to make it yours.",
|
|
62
|
+
color: "#234f45",
|
|
63
|
+
features: [
|
|
64
|
+
"Eight original products",
|
|
65
|
+
"Search, sort, and filters",
|
|
66
|
+
"Persistent shopping cart"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
var deployments = deployments_default;
|
|
71
|
+
var variants = templates.flatMap(
|
|
72
|
+
(template) => frameworks.map((framework) => {
|
|
73
|
+
const id = `${template.id}/${framework}`;
|
|
74
|
+
return {
|
|
75
|
+
schemaVersion: 1,
|
|
76
|
+
id,
|
|
77
|
+
template: template.id,
|
|
78
|
+
framework,
|
|
79
|
+
version: templateVersion,
|
|
80
|
+
sdkVersion,
|
|
81
|
+
sourceDirectory: id,
|
|
82
|
+
rendering: framework === "tanstack-start" ? "ssr" : "spa",
|
|
83
|
+
locales: ["en", "fr", "es", "ar"],
|
|
84
|
+
sourceUrl: `https://unpkg.com/@linguanahq/cli@${templateSourcePackageVersion}/dist/templates/${id}/`,
|
|
85
|
+
demoUrl: deployments[id]?.version === templateVersion ? deployments[id]?.url : void 0,
|
|
86
|
+
screenshot: `/templates/${template.id}-${framework}.png`,
|
|
87
|
+
deploymentTargets: framework === "tanstack-start" ? ["cloudflare", "node"] : ["cloudflare", "static"]
|
|
88
|
+
};
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
function normalizeFramework(value) {
|
|
92
|
+
const normalized = value === "solid" ? "solidjs" : value;
|
|
93
|
+
return frameworks.includes(normalized) ? normalized : void 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ../templates/src/scaffold.ts
|
|
97
|
+
import {
|
|
98
|
+
cp,
|
|
99
|
+
lstat,
|
|
100
|
+
mkdir,
|
|
101
|
+
readdir,
|
|
102
|
+
readFile,
|
|
103
|
+
realpath,
|
|
104
|
+
writeFile
|
|
105
|
+
} from "node:fs/promises";
|
|
106
|
+
import { basename, dirname, resolve } from "node:path";
|
|
107
|
+
async function validateDestination(directory) {
|
|
108
|
+
const target = resolve(directory);
|
|
109
|
+
if (target === dirname(target))
|
|
110
|
+
throw new Error("Choose an application directory, not a filesystem root.");
|
|
111
|
+
let current = target;
|
|
112
|
+
while (current !== dirname(current)) {
|
|
113
|
+
const stat2 = await lstat(current).catch(() => void 0);
|
|
114
|
+
if (stat2?.isSymbolicLink())
|
|
115
|
+
throw new Error(
|
|
116
|
+
"Application directories must not pass through symbolic links."
|
|
117
|
+
);
|
|
118
|
+
current = dirname(current);
|
|
119
|
+
}
|
|
120
|
+
const stat = await lstat(target).catch(() => void 0);
|
|
121
|
+
if (stat && !stat.isDirectory())
|
|
122
|
+
throw new Error("The destination is not a directory.");
|
|
123
|
+
if (stat && (await readdir(target)).some((name) => name !== ".git"))
|
|
124
|
+
throw new Error(
|
|
125
|
+
"The destination is not empty. Choose a new directory, or use linguana link for an existing app."
|
|
126
|
+
);
|
|
127
|
+
return target;
|
|
128
|
+
}
|
|
129
|
+
async function scaffold(input) {
|
|
130
|
+
const target = await validateDestination(input.directory);
|
|
131
|
+
const variant = variants.find(
|
|
132
|
+
(item) => item.framework === input.framework && item.template === input.template
|
|
133
|
+
);
|
|
134
|
+
if (!variant) throw new Error("Unsupported framework/template combination.");
|
|
135
|
+
const sourceRoot = resolve(
|
|
136
|
+
input.sourceRoot ?? resolve(import.meta.dirname, "../sources")
|
|
137
|
+
);
|
|
138
|
+
const source = resolve(sourceRoot, variant.sourceDirectory);
|
|
139
|
+
if (!(await realpath(source)).startsWith(`${await realpath(sourceRoot)}/`))
|
|
140
|
+
throw new Error("Invalid template source path.");
|
|
141
|
+
await mkdir(target, { recursive: true });
|
|
142
|
+
for (const entry of await readdir(source)) {
|
|
143
|
+
if (["node_modules", ".output", "dist", ".linguana", ".env.local"].includes(
|
|
144
|
+
entry
|
|
145
|
+
))
|
|
146
|
+
continue;
|
|
147
|
+
await cp(
|
|
148
|
+
resolve(source, entry),
|
|
149
|
+
resolve(target, entry === "_gitignore" ? ".gitignore" : entry),
|
|
150
|
+
{
|
|
151
|
+
recursive: true,
|
|
152
|
+
errorOnExist: true,
|
|
153
|
+
force: false,
|
|
154
|
+
dereference: true
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const manifestPath = resolve(target, "package.json");
|
|
159
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
160
|
+
manifest.name = basename(target).toLowerCase().replace(/[^a-z0-9._-]/g, "-").replace(/^[._-]+/, "") || "linguana-app";
|
|
161
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
162
|
+
`);
|
|
163
|
+
return { directory: target, variant };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/index.ts
|
|
167
|
+
import { Command, CommanderError } from "commander";
|
|
168
|
+
|
|
169
|
+
// src/cancellation.ts
|
|
170
|
+
var cancellation = new AbortController();
|
|
171
|
+
|
|
172
|
+
// src/errors.ts
|
|
173
|
+
var CliError = class extends Error {
|
|
174
|
+
constructor(message, code = "COMMAND_FAILED", exitCode = 1) {
|
|
175
|
+
super(message);
|
|
176
|
+
this.code = code;
|
|
177
|
+
this.exitCode = exitCode;
|
|
178
|
+
this.name = "CliError";
|
|
179
|
+
}
|
|
180
|
+
code;
|
|
181
|
+
exitCode;
|
|
182
|
+
};
|
|
183
|
+
function safeMessage(error) {
|
|
184
|
+
const text2 = error instanceof Error ? error.message : "The command failed.";
|
|
185
|
+
return text2.replace(/\blna_[\w.-]+/g, "[REDACTED]").replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/api.ts
|
|
189
|
+
function apiOrigin(value = defaultApiUrl) {
|
|
190
|
+
let url;
|
|
191
|
+
try {
|
|
192
|
+
url = new URL(value);
|
|
193
|
+
} catch {
|
|
194
|
+
throw new CliError("Enter a valid API origin.", "INVALID_API_URL", 2);
|
|
195
|
+
}
|
|
196
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
197
|
+
if (url.protocol !== "https:" && !(local && url.protocol === "http:") || url.username || url.password || url.search || url.hash || url.pathname !== "/")
|
|
198
|
+
throw new CliError(
|
|
199
|
+
"The API must be an HTTPS origin, or an HTTP loopback origin for development.",
|
|
200
|
+
"INVALID_API_URL",
|
|
201
|
+
2
|
|
202
|
+
);
|
|
203
|
+
return url.origin;
|
|
204
|
+
}
|
|
205
|
+
var ApiError = class extends CliError {
|
|
206
|
+
constructor(status, code, message, requestId) {
|
|
207
|
+
super(`${message}${requestId ? ` (request ${requestId})` : ""}`, code);
|
|
208
|
+
this.status = status;
|
|
209
|
+
this.requestId = requestId;
|
|
210
|
+
}
|
|
211
|
+
status;
|
|
212
|
+
requestId;
|
|
213
|
+
};
|
|
214
|
+
var Api = class {
|
|
215
|
+
constructor(origin, token, fetcher = fetch) {
|
|
216
|
+
this.token = token;
|
|
217
|
+
this.fetcher = fetcher;
|
|
218
|
+
this.origin = apiOrigin(origin);
|
|
219
|
+
}
|
|
220
|
+
token;
|
|
221
|
+
fetcher;
|
|
222
|
+
origin;
|
|
223
|
+
async request(path, options = {}) {
|
|
224
|
+
if (!path.startsWith("/") || path.startsWith("//") || new URL(path, this.origin).origin !== this.origin)
|
|
225
|
+
throw new CliError("Invalid API request path.", "INVALID_API_PATH", 2);
|
|
226
|
+
const response = await this.fetcher(new URL(path, this.origin), {
|
|
227
|
+
method: options.method ?? "GET",
|
|
228
|
+
redirect: "error",
|
|
229
|
+
signal: AbortSignal.any([
|
|
230
|
+
cancellation.signal,
|
|
231
|
+
AbortSignal.timeout(3e4),
|
|
232
|
+
...options.signal ? [options.signal] : []
|
|
233
|
+
]),
|
|
234
|
+
headers: {
|
|
235
|
+
"content-type": "application/json",
|
|
236
|
+
"user-agent": "linguana-cli/0.1.0",
|
|
237
|
+
...this.token ? { authorization: `Bearer ${this.token}` } : {},
|
|
238
|
+
...options.key ? { "idempotency-key": options.key } : {}
|
|
239
|
+
},
|
|
240
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
241
|
+
});
|
|
242
|
+
const body = await response.json().catch(() => ({}));
|
|
243
|
+
if (!response.ok)
|
|
244
|
+
throw new ApiError(
|
|
245
|
+
response.status,
|
|
246
|
+
String(body.code ?? body.error ?? `HTTP_${response.status}`),
|
|
247
|
+
String(
|
|
248
|
+
body.message ?? body.error_description ?? (response.status === 401 ? "Your login has expired. Run linguana login." : `Linguana returned HTTP ${response.status}.`)
|
|
249
|
+
),
|
|
250
|
+
typeof body.requestId === "string" ? body.requestId : void 0
|
|
251
|
+
);
|
|
252
|
+
return body;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/auth.ts
|
|
257
|
+
import { createHash } from "node:crypto";
|
|
258
|
+
import { rm as rm2 } from "node:fs/promises";
|
|
259
|
+
import { join as join2 } from "node:path";
|
|
260
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
261
|
+
import open from "open";
|
|
262
|
+
|
|
263
|
+
// src/files.ts
|
|
264
|
+
import { execFileSync } from "node:child_process";
|
|
265
|
+
import { randomUUID } from "node:crypto";
|
|
266
|
+
import {
|
|
267
|
+
chmod,
|
|
268
|
+
lstat as lstat2,
|
|
269
|
+
mkdir as mkdir2,
|
|
270
|
+
readFile as readFile2,
|
|
271
|
+
rename,
|
|
272
|
+
rm,
|
|
273
|
+
writeFile as writeFile2
|
|
274
|
+
} from "node:fs/promises";
|
|
275
|
+
import { homedir } from "node:os";
|
|
276
|
+
import { dirname as dirname2, join, resolve as resolve2 } from "node:path";
|
|
277
|
+
function configDirectory() {
|
|
278
|
+
if (process.env.LINGUANA_CONFIG_DIR)
|
|
279
|
+
return resolve2(process.env.LINGUANA_CONFIG_DIR);
|
|
280
|
+
if (process.platform === "win32")
|
|
281
|
+
return join(
|
|
282
|
+
process.env.APPDATA ?? join(homedir(), "AppData/Roaming"),
|
|
283
|
+
"Linguana"
|
|
284
|
+
);
|
|
285
|
+
if (process.platform === "darwin")
|
|
286
|
+
return join(homedir(), "Library/Application Support/Linguana");
|
|
287
|
+
return join(
|
|
288
|
+
process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"),
|
|
289
|
+
"linguana"
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
async function assertNoSymlink(path) {
|
|
293
|
+
let current = resolve2(path);
|
|
294
|
+
while (current !== dirname2(current)) {
|
|
295
|
+
if ((await lstat2(current).catch(() => void 0))?.isSymbolicLink())
|
|
296
|
+
throw new CliError(
|
|
297
|
+
`Refusing to write through a symbolic link: ${current}`,
|
|
298
|
+
"UNSAFE_PATH",
|
|
299
|
+
2
|
|
300
|
+
);
|
|
301
|
+
current = dirname2(current);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function atomicWrite(path, contents, secret = false) {
|
|
305
|
+
await assertNoSymlink(path);
|
|
306
|
+
await mkdir2(dirname2(path), { recursive: true, mode: 448 });
|
|
307
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
308
|
+
try {
|
|
309
|
+
await writeFile2(temporary, contents, {
|
|
310
|
+
flag: "wx",
|
|
311
|
+
mode: secret ? 384 : 420
|
|
312
|
+
});
|
|
313
|
+
await rename(temporary, path);
|
|
314
|
+
if (secret) await chmod(path, 384);
|
|
315
|
+
} finally {
|
|
316
|
+
await rm(temporary, { force: true });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function readJson(path) {
|
|
320
|
+
try {
|
|
321
|
+
return JSON.parse(await readFile2(path, "utf8"));
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (error.code === "ENOENT") return void 0;
|
|
324
|
+
throw new CliError(
|
|
325
|
+
`Cannot read ${path}. Repair the file before continuing.`,
|
|
326
|
+
"INVALID_CONFIG",
|
|
327
|
+
2
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
var writeJson = (path, value, secret = false) => atomicWrite(path, `${JSON.stringify(value, null, 2)}
|
|
332
|
+
`, secret);
|
|
333
|
+
function isGitTracked(directory, file) {
|
|
334
|
+
try {
|
|
335
|
+
return execFileSync("git", ["ls-files", "--error-unmatch", "--", file], {
|
|
336
|
+
cwd: directory,
|
|
337
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
338
|
+
}).length > 0;
|
|
339
|
+
} catch {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function parseEnv(source) {
|
|
344
|
+
const values = {};
|
|
345
|
+
for (const line of source.split(/\r?\n/)) {
|
|
346
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(
|
|
347
|
+
line
|
|
348
|
+
);
|
|
349
|
+
if (!match?.[1]) continue;
|
|
350
|
+
let value = (match[2] ?? "").trim();
|
|
351
|
+
const quoted = /^("(?:\\.|[^"\\])*"|'[^']*')\s*(?:#.*)?$/.exec(value)?.[1];
|
|
352
|
+
if (quoted) value = quoted;
|
|
353
|
+
if (quoted?.startsWith('"')) {
|
|
354
|
+
try {
|
|
355
|
+
value = JSON.parse(value);
|
|
356
|
+
} catch {
|
|
357
|
+
value = value.slice(1, -1).replace(/\\n/g, "\n").replace(/\\r/g, "\r");
|
|
358
|
+
}
|
|
359
|
+
} else if (quoted?.startsWith("'")) value = value.slice(1, -1);
|
|
360
|
+
else value = value.replace(/#.*$/, "").trim();
|
|
361
|
+
values[match[1]] = value;
|
|
362
|
+
}
|
|
363
|
+
return values;
|
|
364
|
+
}
|
|
365
|
+
function mergeEnv(source, updates) {
|
|
366
|
+
const newline = source.includes("\r\n") ? "\r\n" : "\n";
|
|
367
|
+
const seen = /* @__PURE__ */ new Set();
|
|
368
|
+
const lines = source.split(/\r?\n/).map((line) => {
|
|
369
|
+
const match = /^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)(\s*=).*?$/.exec(
|
|
370
|
+
line
|
|
371
|
+
);
|
|
372
|
+
const key = match?.[2];
|
|
373
|
+
if (!key || !(key in updates)) return line;
|
|
374
|
+
if (seen.has(key))
|
|
375
|
+
throw new CliError(
|
|
376
|
+
`Duplicate environment variable ${key}; remove the duplicate before continuing.`,
|
|
377
|
+
"DUPLICATE_ENV",
|
|
378
|
+
2
|
|
379
|
+
);
|
|
380
|
+
seen.add(key);
|
|
381
|
+
const comment = /\s+(#.*)$/.exec(line)?.[1];
|
|
382
|
+
return `${match?.[1] ?? ""}${key}=${JSON.stringify(updates[key])}${comment ? ` ${comment}` : ""}`;
|
|
383
|
+
});
|
|
384
|
+
while (lines.at(-1) === "") lines.pop();
|
|
385
|
+
for (const [key, value] of Object.entries(updates))
|
|
386
|
+
if (!seen.has(key)) lines.push(`${key}=${JSON.stringify(value)}`);
|
|
387
|
+
return `${lines.join(newline)}${newline}`;
|
|
388
|
+
}
|
|
389
|
+
async function ensureGitignore(directory) {
|
|
390
|
+
const path = join(directory, ".gitignore");
|
|
391
|
+
const source = await readFile2(path, "utf8").catch(() => "");
|
|
392
|
+
const additions = [".env.local", ".env.*.local", ".linguana/"].filter(
|
|
393
|
+
(line) => !source.split(/\r?\n/).includes(line)
|
|
394
|
+
);
|
|
395
|
+
if (additions.length)
|
|
396
|
+
await atomicWrite(
|
|
397
|
+
path,
|
|
398
|
+
`${source}${source.endsWith("\n") || !source ? "" : "\n"}${additions.join("\n")}
|
|
399
|
+
`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/auth.ts
|
|
404
|
+
var credentialPath = (origin) => join2(
|
|
405
|
+
configDirectory(),
|
|
406
|
+
`${createHash("sha256").update(origin).digest("hex")}.json`
|
|
407
|
+
);
|
|
408
|
+
async function savedToken(origin) {
|
|
409
|
+
const supplied = process.env.LINGUANA_AUTH_TOKEN;
|
|
410
|
+
if (supplied) {
|
|
411
|
+
if (supplied.startsWith("lna_"))
|
|
412
|
+
throw new CliError(
|
|
413
|
+
"LINGUANA_AUTH_TOKEN expects an account session, not a project build token.",
|
|
414
|
+
"WRONG_TOKEN_TYPE",
|
|
415
|
+
2
|
|
416
|
+
);
|
|
417
|
+
return supplied;
|
|
418
|
+
}
|
|
419
|
+
const value = await readJson(credentialPath(origin));
|
|
420
|
+
if (!value) return void 0;
|
|
421
|
+
if (value.schemaVersion !== 1 || value.origin !== origin || typeof value.token !== "string" || !Number.isFinite(Date.parse(value.expiresAt)))
|
|
422
|
+
throw new CliError(
|
|
423
|
+
"Saved login is invalid. Run linguana login.",
|
|
424
|
+
"INVALID_CREDENTIALS"
|
|
425
|
+
);
|
|
426
|
+
if (Date.parse(value.expiresAt) <= Date.now()) {
|
|
427
|
+
await rm2(credentialPath(origin), { force: true });
|
|
428
|
+
return void 0;
|
|
429
|
+
}
|
|
430
|
+
return value.token;
|
|
431
|
+
}
|
|
432
|
+
async function login(origin, ui, options = {}) {
|
|
433
|
+
const api = options.api ?? new Api(origin);
|
|
434
|
+
const data = await api.request("/api/auth/device/code", {
|
|
435
|
+
method: "POST",
|
|
436
|
+
body: { client_id: "linguana-cli" },
|
|
437
|
+
signal: options.signal ?? cancellation.signal
|
|
438
|
+
});
|
|
439
|
+
const verification = new URL(
|
|
440
|
+
data.verification_uri_complete ?? data.verification_uri
|
|
441
|
+
);
|
|
442
|
+
apiOrigin(verification.origin);
|
|
443
|
+
if (!data.device_code || !data.user_code || !Number.isFinite(data.expires_in) || !Number.isFinite(data.interval))
|
|
444
|
+
throw new CliError(
|
|
445
|
+
"The server returned an invalid device authorization response.",
|
|
446
|
+
"INVALID_AUTH_RESPONSE"
|
|
447
|
+
);
|
|
448
|
+
ui.notice(
|
|
449
|
+
`Open ${verification.href}
|
|
450
|
+
Confirm code: ${data.user_code}
|
|
451
|
+
Sign in or create an account, then authorize your terminal.`
|
|
452
|
+
);
|
|
453
|
+
if (options.browser !== false)
|
|
454
|
+
await (options.openBrowser ?? open)(verification.href).catch(
|
|
455
|
+
() => ui.notice("Could not open a browser. Open the address above manually.")
|
|
456
|
+
);
|
|
457
|
+
const deadline = Date.now() + Math.min(data.expires_in, 600) * 1e3;
|
|
458
|
+
let interval = Math.max(5, data.interval);
|
|
459
|
+
while (Date.now() < deadline) {
|
|
460
|
+
await delay(interval * 1e3, void 0, {
|
|
461
|
+
signal: options.signal ?? cancellation.signal
|
|
462
|
+
});
|
|
463
|
+
try {
|
|
464
|
+
const result = await api.request("/api/auth/device/token", {
|
|
465
|
+
method: "POST",
|
|
466
|
+
body: {
|
|
467
|
+
client_id: "linguana-cli",
|
|
468
|
+
device_code: data.device_code,
|
|
469
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
470
|
+
},
|
|
471
|
+
signal: options.signal ?? cancellation.signal
|
|
472
|
+
});
|
|
473
|
+
if (!result.access_token || !Number.isFinite(result.expires_in) || result.expires_in <= 0)
|
|
474
|
+
throw new CliError(
|
|
475
|
+
"The server returned an invalid CLI session.",
|
|
476
|
+
"INVALID_AUTH_RESPONSE"
|
|
477
|
+
);
|
|
478
|
+
await writeJson(
|
|
479
|
+
credentialPath(origin),
|
|
480
|
+
{
|
|
481
|
+
schemaVersion: 1,
|
|
482
|
+
origin,
|
|
483
|
+
token: result.access_token,
|
|
484
|
+
expiresAt: new Date(
|
|
485
|
+
Date.now() + result.expires_in * 1e3
|
|
486
|
+
).toISOString()
|
|
487
|
+
},
|
|
488
|
+
true
|
|
489
|
+
);
|
|
490
|
+
return new Api(origin, result.access_token, api.fetcher);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (!(error instanceof ApiError)) throw error;
|
|
493
|
+
if (error.code === "authorization_pending") continue;
|
|
494
|
+
if (error.code === "slow_down" || error.status === 429) {
|
|
495
|
+
interval += 5;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (error.code === "access_denied")
|
|
499
|
+
throw new CliError(
|
|
500
|
+
"Authorization was denied. Run linguana login to try again.",
|
|
501
|
+
"ACCESS_DENIED"
|
|
502
|
+
);
|
|
503
|
+
if (error.code === "expired_token")
|
|
504
|
+
throw new CliError(
|
|
505
|
+
"The login code expired. Run linguana login again.",
|
|
506
|
+
"LOGIN_EXPIRED"
|
|
507
|
+
);
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
throw new CliError(
|
|
512
|
+
"The login code expired. Run linguana login again.",
|
|
513
|
+
"LOGIN_EXPIRED"
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
async function authenticatedApi(origin, ui) {
|
|
517
|
+
const token = await savedToken(origin);
|
|
518
|
+
if (!token) {
|
|
519
|
+
if (ui.interactive) return login(origin, ui);
|
|
520
|
+
throw new CliError(
|
|
521
|
+
"Sign in with linguana login first, or use --demo to create an app without an account.",
|
|
522
|
+
"LOGIN_REQUIRED"
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const api = new Api(origin, token);
|
|
526
|
+
const session = await api.request("/api/auth/get-session");
|
|
527
|
+
if (!session?.user)
|
|
528
|
+
throw new CliError(
|
|
529
|
+
"Your session has expired or was revoked. Run linguana login.",
|
|
530
|
+
"LOGIN_REQUIRED"
|
|
531
|
+
);
|
|
532
|
+
return api;
|
|
533
|
+
}
|
|
534
|
+
async function logout(origin) {
|
|
535
|
+
const token = await savedToken(origin);
|
|
536
|
+
let failure;
|
|
537
|
+
try {
|
|
538
|
+
if (token)
|
|
539
|
+
await new Api(origin, token).request("/api/auth/sign-out", {
|
|
540
|
+
method: "POST",
|
|
541
|
+
body: {}
|
|
542
|
+
});
|
|
543
|
+
} catch (error) {
|
|
544
|
+
failure = error;
|
|
545
|
+
}
|
|
546
|
+
await rm2(credentialPath(origin), { force: true });
|
|
547
|
+
if (failure)
|
|
548
|
+
throw new CliError(
|
|
549
|
+
"Local login removed, but the server could not revoke the session. Revoke it in account settings when the server is available.",
|
|
550
|
+
"REMOTE_LOGOUT_FAILED"
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/doctor.ts
|
|
555
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
556
|
+
import { join as join4 } from "node:path";
|
|
557
|
+
|
|
558
|
+
// src/project.ts
|
|
559
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
560
|
+
import { constants } from "node:fs";
|
|
561
|
+
import { access, mkdir as mkdir3, open as open2, readFile as readFile3, rm as rm3 } from "node:fs/promises";
|
|
562
|
+
import { hostname } from "node:os";
|
|
563
|
+
import { basename as basename2, join as join3, resolve as resolve3 } from "node:path";
|
|
564
|
+
async function readProject(directory) {
|
|
565
|
+
const config = await readJson(
|
|
566
|
+
join3(directory, "linguana.json")
|
|
567
|
+
);
|
|
568
|
+
if (config && (config.schemaVersion !== 1 || !normalizeFramework(config.framework) || !["demo", "connected"].includes(config.mode) || !config.sourceLocale || !config.defaultEnvironment))
|
|
569
|
+
throw new CliError(
|
|
570
|
+
"linguana.json is invalid or uses an unsupported schema version.",
|
|
571
|
+
"INVALID_PROJECT_CONFIG",
|
|
572
|
+
2
|
|
573
|
+
);
|
|
574
|
+
return config;
|
|
575
|
+
}
|
|
576
|
+
async function detectFramework(directory, provided) {
|
|
577
|
+
if (provided) {
|
|
578
|
+
const value = normalizeFramework(provided);
|
|
579
|
+
if (value) return value;
|
|
580
|
+
throw new CliError("Unsupported framework.", "INVALID_FRAMEWORK", 2);
|
|
581
|
+
}
|
|
582
|
+
const manifest = await readJson(join3(directory, "package.json"));
|
|
583
|
+
if (!manifest)
|
|
584
|
+
throw new CliError(
|
|
585
|
+
"No package.json found. Run this command in an app directory.",
|
|
586
|
+
"APP_NOT_FOUND",
|
|
587
|
+
2
|
|
588
|
+
);
|
|
589
|
+
const dependencies = {
|
|
590
|
+
...manifest.dependencies,
|
|
591
|
+
...manifest.devDependencies
|
|
592
|
+
};
|
|
593
|
+
if (dependencies["@tanstack/react-start"]) return "tanstack-start";
|
|
594
|
+
const candidates = [
|
|
595
|
+
["react", "react"],
|
|
596
|
+
["vue", "vue"],
|
|
597
|
+
["svelte", "svelte"],
|
|
598
|
+
["solid-js", "solidjs"]
|
|
599
|
+
].filter(([dependency]) => dependencies[dependency]);
|
|
600
|
+
const candidate = candidates[0];
|
|
601
|
+
if (candidates.length !== 1 || !candidate)
|
|
602
|
+
throw new CliError(
|
|
603
|
+
"Could not identify one framework. Pass --framework explicitly.",
|
|
604
|
+
"FRAMEWORK_REQUIRED",
|
|
605
|
+
2
|
|
606
|
+
);
|
|
607
|
+
return candidate[1];
|
|
608
|
+
}
|
|
609
|
+
function environmentFile(environment) {
|
|
610
|
+
if (!/^[a-z][a-z0-9-]{0,39}$/.test(environment))
|
|
611
|
+
throw new CliError("Invalid environment name.", "INVALID_ENVIRONMENT", 2);
|
|
612
|
+
return environment === "development" ? ".env.local" : `.env.${environment}.local`;
|
|
613
|
+
}
|
|
614
|
+
function slug(value) {
|
|
615
|
+
const result = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 55).replace(/-$/, "");
|
|
616
|
+
if (!result)
|
|
617
|
+
throw new CliError(
|
|
618
|
+
"The name needs at least one letter or number.",
|
|
619
|
+
"INVALID_NAME",
|
|
620
|
+
2
|
|
621
|
+
);
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
async function connectProject(directory, options, ui, suppliedApi) {
|
|
625
|
+
const root = resolve3(directory);
|
|
626
|
+
await assertNoSymlink(root);
|
|
627
|
+
await assertNoSymlink(join3(root, "linguana.json"));
|
|
628
|
+
await assertNoSymlink(join3(root, ".linguana/setup.json"));
|
|
629
|
+
const previous = await readProject(root);
|
|
630
|
+
const framework = await detectFramework(
|
|
631
|
+
root,
|
|
632
|
+
options.framework ?? previous?.framework
|
|
633
|
+
);
|
|
634
|
+
const origin = apiOrigin(options.apiUrl ?? previous?.apiUrl ?? defaultApiUrl);
|
|
635
|
+
const environment = options.environment ?? previous?.defaultEnvironment ?? "development";
|
|
636
|
+
const envName = environmentFile(environment);
|
|
637
|
+
const envPath = join3(root, envName);
|
|
638
|
+
await assertNoSymlink(envPath);
|
|
639
|
+
if (isGitTracked(root, envName))
|
|
640
|
+
throw new CliError(
|
|
641
|
+
`${envName} is tracked by Git. Remove it from the index before writing credentials.`,
|
|
642
|
+
"TRACKED_ENV",
|
|
643
|
+
2
|
|
644
|
+
);
|
|
645
|
+
await ensureGitignore(root);
|
|
646
|
+
await assertNoSymlink(join3(root, ".linguana"));
|
|
647
|
+
await mkdir3(join3(root, ".linguana"), { recursive: true, mode: 448 });
|
|
648
|
+
await access(root, constants.W_OK);
|
|
649
|
+
await access(join3(root, ".linguana"), constants.W_OK);
|
|
650
|
+
for (const name of ["linguana.json", ".linguana/setup.json"])
|
|
651
|
+
await access(join3(root, name), constants.W_OK).catch((error) => {
|
|
652
|
+
if (error.code !== "ENOENT")
|
|
653
|
+
throw new CliError(
|
|
654
|
+
`Cannot write ${name}. Fix local permissions before connecting.`,
|
|
655
|
+
"CONFIG_NOT_WRITABLE"
|
|
656
|
+
);
|
|
657
|
+
});
|
|
658
|
+
await access(envPath, constants.W_OK).catch((error) => {
|
|
659
|
+
if (error.code !== "ENOENT")
|
|
660
|
+
throw new CliError(
|
|
661
|
+
`Cannot write ${envName}. Fix local permissions before connecting.`,
|
|
662
|
+
"ENV_NOT_WRITABLE"
|
|
663
|
+
);
|
|
664
|
+
});
|
|
665
|
+
const lockPath = join3(root, ".linguana/cli.lock");
|
|
666
|
+
let lock;
|
|
667
|
+
try {
|
|
668
|
+
lock = await open2(lockPath, "wx", 384);
|
|
669
|
+
} catch {
|
|
670
|
+
const owner = await readJson(lockPath);
|
|
671
|
+
let stale = false;
|
|
672
|
+
if (owner?.hostname === hostname() && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
673
|
+
try {
|
|
674
|
+
process.kill(owner.pid, 0);
|
|
675
|
+
} catch (error) {
|
|
676
|
+
stale = error.code === "ESRCH";
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (!stale)
|
|
680
|
+
throw new CliError(
|
|
681
|
+
"Another setup is running. If it was interrupted on another machine, remove .linguana/cli.lock and retry.",
|
|
682
|
+
"SETUP_LOCKED"
|
|
683
|
+
);
|
|
684
|
+
await rm3(lockPath);
|
|
685
|
+
lock = await open2(lockPath, "wx", 384);
|
|
686
|
+
}
|
|
687
|
+
await lock.writeFile(
|
|
688
|
+
JSON.stringify({ pid: process.pid, hostname: hostname() })
|
|
689
|
+
);
|
|
690
|
+
try {
|
|
691
|
+
const api = suppliedApi ?? await authenticatedApi(origin, ui);
|
|
692
|
+
const links = await api.request(
|
|
693
|
+
"/api/v1/cli/config"
|
|
694
|
+
);
|
|
695
|
+
const projectUrl = new URL(
|
|
696
|
+
"/dashboard",
|
|
697
|
+
apiOrigin(new URL(links.dashboardUrl).origin)
|
|
698
|
+
);
|
|
699
|
+
const journalPath = join3(root, ".linguana/setup.json");
|
|
700
|
+
const stored = await readJson(journalPath);
|
|
701
|
+
const journal = stored?.origin === origin ? stored : { schemaVersion: 1, origin, keys: {} };
|
|
702
|
+
const key = async (scope) => {
|
|
703
|
+
if (!journal.keys[scope]) {
|
|
704
|
+
journal.keys[scope] = randomUUID2();
|
|
705
|
+
await writeJson(journalPath, journal);
|
|
706
|
+
}
|
|
707
|
+
return journal.keys[scope];
|
|
708
|
+
};
|
|
709
|
+
const organizations = await api.request(
|
|
710
|
+
"/api/v1/organizations"
|
|
711
|
+
);
|
|
712
|
+
let projectId = options.project ?? (!options.newProject && previous?.apiUrl === origin ? previous.projectId : void 0) ?? (!options.newProject ? journal.projectId : void 0);
|
|
713
|
+
let project = projectId ? await api.request(
|
|
714
|
+
`/api/v1/projects/${encodeURIComponent(projectId)}/settings`
|
|
715
|
+
) : void 0;
|
|
716
|
+
let organization = options.newOrganization ? void 0 : organizations.find(
|
|
717
|
+
(item) => item.id === (options.organization ?? project?.organizationId ?? journal.organizationId)
|
|
718
|
+
);
|
|
719
|
+
if (options.organization && !organization)
|
|
720
|
+
throw new CliError(
|
|
721
|
+
"Organization not found in your account.",
|
|
722
|
+
"ORGANIZATION_NOT_FOUND"
|
|
723
|
+
);
|
|
724
|
+
if (project && options.organization && project.organizationId !== options.organization)
|
|
725
|
+
throw new CliError(
|
|
726
|
+
"The project belongs to a different organization.",
|
|
727
|
+
"PROJECT_SCOPE_MISMATCH",
|
|
728
|
+
2
|
|
729
|
+
);
|
|
730
|
+
if (options.newOrganization && project)
|
|
731
|
+
throw new CliError(
|
|
732
|
+
"A new organization cannot be combined with an existing project.",
|
|
733
|
+
"CONFLICTING_OPTIONS",
|
|
734
|
+
2
|
|
735
|
+
);
|
|
736
|
+
if (!organization) {
|
|
737
|
+
const selection = options.newOrganization || organizations.length === 0 ? "new" : await ui.choose("Choose an organization", [
|
|
738
|
+
...organizations.map((item) => ({
|
|
739
|
+
value: item.id,
|
|
740
|
+
label: item.name
|
|
741
|
+
})),
|
|
742
|
+
...ui.interactive ? [{ value: "new", label: "Create an organization" }] : []
|
|
743
|
+
]);
|
|
744
|
+
if (selection === "new") {
|
|
745
|
+
if (!options.newOrganization && !ui.interactive)
|
|
746
|
+
throw new CliError(
|
|
747
|
+
"Pass --new-organization <name> to create your first organization.",
|
|
748
|
+
"ORGANIZATION_REQUIRED",
|
|
749
|
+
2
|
|
750
|
+
);
|
|
751
|
+
const session = await api.request("/api/auth/get-session");
|
|
752
|
+
const name = options.newOrganization ?? await ui.input(
|
|
753
|
+
"Organization name",
|
|
754
|
+
`${session.user.name}'s workspace`
|
|
755
|
+
);
|
|
756
|
+
const created = await api.request(
|
|
757
|
+
"/api/v1/organizations",
|
|
758
|
+
{
|
|
759
|
+
method: "POST",
|
|
760
|
+
body: { name, slug: slug(name) },
|
|
761
|
+
key: await key(`organization:${name}`)
|
|
762
|
+
}
|
|
763
|
+
);
|
|
764
|
+
organization = { ...created, memberships: [{ role: "OWNER" }] };
|
|
765
|
+
} else organization = organizations.find((item) => item.id === selection);
|
|
766
|
+
}
|
|
767
|
+
if (organization?.lifecycleStatus !== "ACTIVE" || !organization.memberships.some(
|
|
768
|
+
(membership) => ["OWNER", "ADMIN"].includes(membership.role)
|
|
769
|
+
))
|
|
770
|
+
throw new CliError(
|
|
771
|
+
"Connecting credentials requires Owner or Admin access to an active organization.",
|
|
772
|
+
"INSUFFICIENT_PERMISSIONS"
|
|
773
|
+
);
|
|
774
|
+
journal.organizationId = organization.id;
|
|
775
|
+
await writeJson(journalPath, journal);
|
|
776
|
+
let createdProject = false;
|
|
777
|
+
if (!project) {
|
|
778
|
+
const projects = await api.request(
|
|
779
|
+
`/api/v1/organizations/${organization.id}/projects`
|
|
780
|
+
);
|
|
781
|
+
const selected = options.newProject ? "new" : await ui.choose(
|
|
782
|
+
"Choose a project",
|
|
783
|
+
[
|
|
784
|
+
...projects.filter((item) => item.lifecycleStatus === "ACTIVE").map((item) => ({ value: item.id, label: item.name })),
|
|
785
|
+
{ value: "new", label: "Create a project" }
|
|
786
|
+
],
|
|
787
|
+
projects.length === 0 ? "new" : void 0
|
|
788
|
+
);
|
|
789
|
+
if (selected === "new") {
|
|
790
|
+
const name = options.newProject ?? await ui.input("Project name", basename2(root));
|
|
791
|
+
project = await api.request(
|
|
792
|
+
`/api/v1/organizations/${organization.id}/projects`,
|
|
793
|
+
{
|
|
794
|
+
method: "POST",
|
|
795
|
+
body: { name, slug: slug(name), sourceLocale: "en" },
|
|
796
|
+
key: await key(`project:${organization.id}:${name}`)
|
|
797
|
+
}
|
|
798
|
+
);
|
|
799
|
+
createdProject = true;
|
|
800
|
+
} else
|
|
801
|
+
project = await api.request(
|
|
802
|
+
`/api/v1/projects/${selected}/settings`
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
if (project.lifecycleStatus !== "ACTIVE")
|
|
806
|
+
throw new CliError(
|
|
807
|
+
"This project is pending deletion.",
|
|
808
|
+
"PROJECT_INACTIVE"
|
|
809
|
+
);
|
|
810
|
+
if (previous?.template && project.sourceLocale !== "en")
|
|
811
|
+
throw new CliError(
|
|
812
|
+
"These templates use English source content. Choose an English-source project.",
|
|
813
|
+
"SOURCE_LOCALE_MISMATCH",
|
|
814
|
+
2
|
|
815
|
+
);
|
|
816
|
+
projectId = project.id;
|
|
817
|
+
journal.projectId = project.id;
|
|
818
|
+
await writeJson(journalPath, journal);
|
|
819
|
+
const targetEnvironment = project.environments.find(
|
|
820
|
+
(item) => item.name === environment
|
|
821
|
+
);
|
|
822
|
+
if (!targetEnvironment)
|
|
823
|
+
throw new CliError(
|
|
824
|
+
"This project does not have the requested environment.",
|
|
825
|
+
"ENVIRONMENT_NOT_FOUND"
|
|
826
|
+
);
|
|
827
|
+
if (createdProject || options.locales !== void 0) {
|
|
828
|
+
const registry = await api.request(`/api/v1/projects/${project.id}/locale-registry`);
|
|
829
|
+
const locales = options.locales !== void 0 ? options.locales.split(",").map((item) => item.trim()).filter(Boolean) : await ui.languages(
|
|
830
|
+
registry.filter((item) => item.code !== project?.sourceLocale).map((item) => ({
|
|
831
|
+
value: item.code,
|
|
832
|
+
label: `${item.displayName} (${item.code})`
|
|
833
|
+
}))
|
|
834
|
+
);
|
|
835
|
+
for (const localeCode of new Set(locales)) {
|
|
836
|
+
if (localeCode === project.sourceLocale) continue;
|
|
837
|
+
if (!registry.some((item) => item.code === localeCode))
|
|
838
|
+
throw new CliError(
|
|
839
|
+
`Unknown locale ${localeCode}.`,
|
|
840
|
+
"INVALID_LOCALE",
|
|
841
|
+
2
|
|
842
|
+
);
|
|
843
|
+
await api.request(`/api/v1/projects/${project.id}/locales`, {
|
|
844
|
+
method: "POST",
|
|
845
|
+
body: {
|
|
846
|
+
localeCode,
|
|
847
|
+
status: "ENABLED",
|
|
848
|
+
fallbackLocale: project.sourceLocale
|
|
849
|
+
},
|
|
850
|
+
key: await key(`locale:${project.id}:${localeCode}`)
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const oldSource = await readFile3(envPath, "utf8").catch((error) => {
|
|
855
|
+
if (error.code === "ENOENT") return "";
|
|
856
|
+
throw error;
|
|
857
|
+
});
|
|
858
|
+
const old = parseEnv(oldSource);
|
|
859
|
+
const updates = {
|
|
860
|
+
LINGUANA_PROJECT_ID: project.id,
|
|
861
|
+
LINGUANA_ENVIRONMENT: environment,
|
|
862
|
+
LINGUANA_API_URL: origin,
|
|
863
|
+
LINGUANA_UPLOAD: "true",
|
|
864
|
+
LINGUANA_TRANSLATE: "false",
|
|
865
|
+
VITE_LINGUANA_PROJECT_ID: project.id,
|
|
866
|
+
VITE_LINGUANA_ENVIRONMENT: environment,
|
|
867
|
+
VITE_LINGUANA_API_URL: origin,
|
|
868
|
+
VITE_LINGUANA_PUBLISHABLE_KEY: project.catalogReadKey
|
|
869
|
+
};
|
|
870
|
+
const conflicts = Object.keys(updates).filter(
|
|
871
|
+
(name) => old[name] !== void 0 && old[name] !== updates[name]
|
|
872
|
+
);
|
|
873
|
+
if (conflicts.length && !options.overwriteEnv && !await ui.confirm(
|
|
874
|
+
`Replace these variables in ${envName}: ${conflicts.join(", ")}?`
|
|
875
|
+
))
|
|
876
|
+
throw new CliError(
|
|
877
|
+
"Environment variables were preserved. Retry with --overwrite-env to replace Linguana values.",
|
|
878
|
+
"ENV_CONFLICT",
|
|
879
|
+
2
|
|
880
|
+
);
|
|
881
|
+
mergeEnv(oldSource, { ...updates, LINGUANA_PROJECT_TOKEN: "pending" });
|
|
882
|
+
let token;
|
|
883
|
+
try {
|
|
884
|
+
if (old.LINGUANA_API_URL && apiOrigin(old.LINGUANA_API_URL) === origin)
|
|
885
|
+
token = old.LINGUANA_PROJECT_TOKEN;
|
|
886
|
+
} catch {
|
|
887
|
+
}
|
|
888
|
+
if (token) {
|
|
889
|
+
try {
|
|
890
|
+
const current = await new Api(origin, token, api.fetcher).request("/api/v1/project-tokens/current");
|
|
891
|
+
if (current.projectId !== project.id || current.environmentId !== targetEnvironment.id)
|
|
892
|
+
token = void 0;
|
|
893
|
+
} catch (error) {
|
|
894
|
+
if (error instanceof CliError && "status" in error && [401, 404].includes(Number(error.status)))
|
|
895
|
+
token = void 0;
|
|
896
|
+
else throw error;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
let createdTokenId;
|
|
900
|
+
if (!token) {
|
|
901
|
+
if (old.LINGUANA_PROJECT_TOKEN && !options.overwriteEnv && !await ui.confirm(
|
|
902
|
+
`Replace the invalid or differently scoped token in ${envName}?`
|
|
903
|
+
))
|
|
904
|
+
throw new CliError(
|
|
905
|
+
"Existing token preserved. Use --overwrite-env to replace it.",
|
|
906
|
+
"ENV_CONFLICT",
|
|
907
|
+
2
|
|
908
|
+
);
|
|
909
|
+
const scope = `token:${project.id}:${environment}`;
|
|
910
|
+
const create = () => api.request(
|
|
911
|
+
`/api/v1/projects/${project?.id}/environments/${environment}/tokens`,
|
|
912
|
+
{
|
|
913
|
+
method: "POST",
|
|
914
|
+
body: { name: `Linguana CLI: ${basename2(root)}`.slice(0, 80) },
|
|
915
|
+
key: journal.keys[scope]
|
|
916
|
+
}
|
|
917
|
+
);
|
|
918
|
+
await key(scope);
|
|
919
|
+
let result = await create();
|
|
920
|
+
if (!result.value) {
|
|
921
|
+
if (!result.id)
|
|
922
|
+
throw new CliError(
|
|
923
|
+
"Token recovery failed. Inspect your project tokens in the dashboard.",
|
|
924
|
+
"TOKEN_RECOVERY_FAILED"
|
|
925
|
+
);
|
|
926
|
+
await api.request(`/api/v1/project-tokens/${result.id}`, {
|
|
927
|
+
method: "DELETE",
|
|
928
|
+
key: await key(`revoke:${result.id}`)
|
|
929
|
+
});
|
|
930
|
+
journal.keys[scope] = randomUUID2();
|
|
931
|
+
await writeJson(journalPath, journal);
|
|
932
|
+
result = await create();
|
|
933
|
+
}
|
|
934
|
+
if (!result.value)
|
|
935
|
+
throw new CliError(
|
|
936
|
+
"The token secret is unavailable. Retry linguana env pull.",
|
|
937
|
+
"TOKEN_SECRET_UNAVAILABLE"
|
|
938
|
+
);
|
|
939
|
+
token = result.value;
|
|
940
|
+
createdTokenId = result.id;
|
|
941
|
+
}
|
|
942
|
+
try {
|
|
943
|
+
await atomicWrite(
|
|
944
|
+
envPath,
|
|
945
|
+
mergeEnv(oldSource, { ...updates, LINGUANA_PROJECT_TOKEN: token }),
|
|
946
|
+
true
|
|
947
|
+
);
|
|
948
|
+
} catch (error) {
|
|
949
|
+
if (createdTokenId)
|
|
950
|
+
await api.request(`/api/v1/project-tokens/${createdTokenId}`, {
|
|
951
|
+
method: "DELETE",
|
|
952
|
+
key: await key(`revoke:${createdTokenId}`)
|
|
953
|
+
}).catch(
|
|
954
|
+
() => ui.notice(
|
|
955
|
+
"The environment file could not be written. Revoke the new CLI token in the dashboard."
|
|
956
|
+
)
|
|
957
|
+
);
|
|
958
|
+
throw error;
|
|
959
|
+
}
|
|
960
|
+
const config = {
|
|
961
|
+
...previous,
|
|
962
|
+
schemaVersion: 1,
|
|
963
|
+
framework,
|
|
964
|
+
mode: "connected",
|
|
965
|
+
apiUrl: origin,
|
|
966
|
+
organizationId: organization.id,
|
|
967
|
+
projectId: project.id,
|
|
968
|
+
sourceLocale: project.sourceLocale,
|
|
969
|
+
defaultEnvironment: previous?.defaultEnvironment ?? environment
|
|
970
|
+
};
|
|
971
|
+
await writeJson(join3(root, "linguana.json"), config);
|
|
972
|
+
projectUrl.searchParams.set("project", project.id);
|
|
973
|
+
return {
|
|
974
|
+
config,
|
|
975
|
+
environment,
|
|
976
|
+
envFile: envName,
|
|
977
|
+
projectId: project.id,
|
|
978
|
+
organizationId: organization.id,
|
|
979
|
+
projectUrl: projectUrl.href
|
|
980
|
+
};
|
|
981
|
+
} finally {
|
|
982
|
+
await lock.close();
|
|
983
|
+
await rm3(lockPath, { force: true });
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/doctor.ts
|
|
988
|
+
async function doctor(directory, requestedEnvironment) {
|
|
989
|
+
const checks = [];
|
|
990
|
+
const config = await readProject(directory);
|
|
991
|
+
const framework = await detectFramework(directory, config?.framework);
|
|
992
|
+
checks.push({ check: "framework", ok: true, message: framework });
|
|
993
|
+
const manifest = await readJson(join4(directory, "package.json"));
|
|
994
|
+
const dependencies = {
|
|
995
|
+
...manifest?.dependencies,
|
|
996
|
+
...manifest?.devDependencies
|
|
997
|
+
};
|
|
998
|
+
const adapter = framework === "solidjs" ? "solid" : framework === "tanstack-start" ? "react" : framework;
|
|
999
|
+
checks.push({
|
|
1000
|
+
check: "sdk",
|
|
1001
|
+
ok: Boolean(
|
|
1002
|
+
dependencies[`@linguanahq/${adapter}`] && dependencies["@linguanahq/vite"]
|
|
1003
|
+
),
|
|
1004
|
+
message: `Install @linguanahq/${adapter} and @linguanahq/vite.`
|
|
1005
|
+
});
|
|
1006
|
+
const vite = await readFile4(join4(directory, "vite.config.ts"), "utf8").catch(
|
|
1007
|
+
() => ""
|
|
1008
|
+
);
|
|
1009
|
+
checks.push({
|
|
1010
|
+
check: "vite",
|
|
1011
|
+
ok: vite.includes("linguana("),
|
|
1012
|
+
message: "Vite configuration must run linguana() before the framework plugin."
|
|
1013
|
+
});
|
|
1014
|
+
checks.push({
|
|
1015
|
+
check: "configuration",
|
|
1016
|
+
ok: Boolean(config),
|
|
1017
|
+
message: config ? `${config.mode} mode` : "Run linguana link to create linguana.json."
|
|
1018
|
+
});
|
|
1019
|
+
if (config?.mode === "connected") {
|
|
1020
|
+
const accountToken = await savedToken(apiOrigin(config.apiUrl));
|
|
1021
|
+
let authenticated = false;
|
|
1022
|
+
if (accountToken) {
|
|
1023
|
+
try {
|
|
1024
|
+
authenticated = Boolean(
|
|
1025
|
+
(await new Api(config.apiUrl, accountToken).request("/api/auth/get-session"))?.user
|
|
1026
|
+
);
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
checks.push({
|
|
1031
|
+
check: "authentication",
|
|
1032
|
+
ok: authenticated,
|
|
1033
|
+
message: authenticated ? "CLI account session is valid." : "Run linguana login to manage this project."
|
|
1034
|
+
});
|
|
1035
|
+
const environment = requestedEnvironment ?? config.defaultEnvironment;
|
|
1036
|
+
const file = environmentFile(environment);
|
|
1037
|
+
const env = parseEnv(
|
|
1038
|
+
await readFile4(join4(directory, file), "utf8").catch(() => "")
|
|
1039
|
+
);
|
|
1040
|
+
const consistent = env.LINGUANA_PROJECT_ID === config.projectId && env.VITE_LINGUANA_PROJECT_ID === config.projectId && env.LINGUANA_ENVIRONMENT === environment && env.VITE_LINGUANA_ENVIRONMENT === environment && env.LINGUANA_API_URL === config.apiUrl && env.VITE_LINGUANA_API_URL === config.apiUrl;
|
|
1041
|
+
checks.push({
|
|
1042
|
+
check: "environment",
|
|
1043
|
+
ok: consistent,
|
|
1044
|
+
message: consistent ? `${file} matches the linked project.` : `Run linguana env pull --environment ${environment} to synchronize ${file}.`
|
|
1045
|
+
});
|
|
1046
|
+
checks.push({
|
|
1047
|
+
check: "git",
|
|
1048
|
+
ok: !isGitTracked(directory, file),
|
|
1049
|
+
message: `${file} must not be tracked by Git.`
|
|
1050
|
+
});
|
|
1051
|
+
checks.push({
|
|
1052
|
+
check: "browser-secrets",
|
|
1053
|
+
ok: !Object.entries(env).some(
|
|
1054
|
+
([key, value]) => key.startsWith("VITE_") && (/TOKEN|SECRET|PASSWORD/.test(key) || value.startsWith("lna_"))
|
|
1055
|
+
),
|
|
1056
|
+
message: "Browser variables must not contain secrets."
|
|
1057
|
+
});
|
|
1058
|
+
if (!env.LINGUANA_PROJECT_TOKEN)
|
|
1059
|
+
checks.push({
|
|
1060
|
+
check: "token",
|
|
1061
|
+
ok: false,
|
|
1062
|
+
message: "No project token found. Run linguana env pull."
|
|
1063
|
+
});
|
|
1064
|
+
else {
|
|
1065
|
+
try {
|
|
1066
|
+
const current = await new Api(
|
|
1067
|
+
apiOrigin(config.apiUrl),
|
|
1068
|
+
env.LINGUANA_PROJECT_TOKEN
|
|
1069
|
+
).request(
|
|
1070
|
+
"/api/v1/project-tokens/current"
|
|
1071
|
+
);
|
|
1072
|
+
checks.push({
|
|
1073
|
+
check: "token",
|
|
1074
|
+
ok: current.projectId === config.projectId && current.environment === environment,
|
|
1075
|
+
message: "The token must be valid and belong to the linked project."
|
|
1076
|
+
});
|
|
1077
|
+
} catch {
|
|
1078
|
+
checks.push({
|
|
1079
|
+
check: "token",
|
|
1080
|
+
ok: false,
|
|
1081
|
+
message: "Could not validate the token. Check connectivity and run linguana env pull."
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return { ok: checks.every((item) => item.ok), checks };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/ui.ts
|
|
1090
|
+
import * as clack from "@clack/prompts";
|
|
1091
|
+
var Ui = class {
|
|
1092
|
+
constructor(options = {}) {
|
|
1093
|
+
this.options = options;
|
|
1094
|
+
this.interactive = Boolean(
|
|
1095
|
+
process.stdin.isTTY && process.stdout.isTTY && !options.yes && !options.json
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
options;
|
|
1099
|
+
interactive;
|
|
1100
|
+
answer(value) {
|
|
1101
|
+
if (clack.isCancel(value))
|
|
1102
|
+
throw new CliError(
|
|
1103
|
+
"Canceled. You can resume project setup with linguana link.",
|
|
1104
|
+
"CANCELED",
|
|
1105
|
+
130
|
|
1106
|
+
);
|
|
1107
|
+
return value;
|
|
1108
|
+
}
|
|
1109
|
+
async input(message, initialValue) {
|
|
1110
|
+
if (!this.interactive) {
|
|
1111
|
+
if (initialValue) return initialValue;
|
|
1112
|
+
throw new CliError(
|
|
1113
|
+
`${message} Supply the corresponding command option.`,
|
|
1114
|
+
"INPUT_REQUIRED",
|
|
1115
|
+
2
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
return this.answer(
|
|
1119
|
+
await clack.text({
|
|
1120
|
+
message,
|
|
1121
|
+
initialValue,
|
|
1122
|
+
validate: (value) => value?.trim() ? void 0 : "Enter a value."
|
|
1123
|
+
})
|
|
1124
|
+
).trim();
|
|
1125
|
+
}
|
|
1126
|
+
async choose(message, options, initialValue) {
|
|
1127
|
+
if (!this.interactive) {
|
|
1128
|
+
if (initialValue) return initialValue;
|
|
1129
|
+
const option = options[0];
|
|
1130
|
+
if (options.length === 1 && option) return option.value;
|
|
1131
|
+
throw new CliError(
|
|
1132
|
+
`${message} Supply an explicit option when running noninteractively.`,
|
|
1133
|
+
"INPUT_REQUIRED",
|
|
1134
|
+
2
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
return this.answer(
|
|
1138
|
+
await clack.select({ message, options, initialValue })
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
async confirm(message, initialValue = false) {
|
|
1142
|
+
if (!this.interactive) return initialValue;
|
|
1143
|
+
return this.answer(await clack.confirm({ message, initialValue }));
|
|
1144
|
+
}
|
|
1145
|
+
async languages(options) {
|
|
1146
|
+
if (!this.interactive)
|
|
1147
|
+
return ["fr", "es", "ar"].filter(
|
|
1148
|
+
(code) => options.some((item) => item.value === code)
|
|
1149
|
+
);
|
|
1150
|
+
return this.answer(
|
|
1151
|
+
await clack.multiselect({
|
|
1152
|
+
message: "Which languages would you like to enable?",
|
|
1153
|
+
options,
|
|
1154
|
+
initialValues: ["fr", "es", "ar"],
|
|
1155
|
+
required: false
|
|
1156
|
+
})
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
async progress(message, task) {
|
|
1160
|
+
const spinner2 = this.interactive ? clack.spinner() : void 0;
|
|
1161
|
+
spinner2?.start(message);
|
|
1162
|
+
try {
|
|
1163
|
+
const value = await task();
|
|
1164
|
+
spinner2?.stop(message);
|
|
1165
|
+
return value;
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
spinner2?.stop("Could not finish this step.");
|
|
1168
|
+
throw error;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
notice(message) {
|
|
1172
|
+
if (this.interactive) clack.log.info(message);
|
|
1173
|
+
else process.stderr.write(`${message}
|
|
1174
|
+
`);
|
|
1175
|
+
}
|
|
1176
|
+
result(value, message) {
|
|
1177
|
+
if (this.options.json) process.stdout.write(`${JSON.stringify(value)}
|
|
1178
|
+
`);
|
|
1179
|
+
else if (this.interactive) clack.outro(message);
|
|
1180
|
+
else process.stdout.write(`${message}
|
|
1181
|
+
`);
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
// src/index.ts
|
|
1186
|
+
process.once("SIGINT", () => {
|
|
1187
|
+
cancellation.abort();
|
|
1188
|
+
process.exitCode = 130;
|
|
1189
|
+
});
|
|
1190
|
+
var program = new Command().name("linguana").description("Create and connect multilingual apps with Linguana.").version("0.1.0").option("--json", "Print structured results without secrets").option("--yes", "Use defaults without prompting").option("--api-url <origin>", "Linguana API origin").showHelpAfterError().exitOverride();
|
|
1191
|
+
var opts = (command) => command.optsWithGlobals();
|
|
1192
|
+
function projectOptions(command) {
|
|
1193
|
+
return command.option("--organization <id>", "Existing organization ID").option("--project <id>", "Existing project ID").option("--new-organization <name>", "Create an organization").option("--new-project <name>", "Create a project").option(
|
|
1194
|
+
"--environment <name>",
|
|
1195
|
+
"Credential environment (default: development)"
|
|
1196
|
+
).option("--framework <framework>", "Framework name").option("--locales <codes>", "Explicit target languages, comma-separated").option("--overwrite-env", "Replace conflicting Linguana variables");
|
|
1197
|
+
}
|
|
1198
|
+
function validateOptions(options) {
|
|
1199
|
+
for (const [left, right] of [
|
|
1200
|
+
["demo", "connect"],
|
|
1201
|
+
["organization", "newOrganization"],
|
|
1202
|
+
["project", "newProject"],
|
|
1203
|
+
["project", "newOrganization"]
|
|
1204
|
+
])
|
|
1205
|
+
if (options[left] && options[right])
|
|
1206
|
+
throw new CliError(
|
|
1207
|
+
`Options ${left} and ${right} cannot be used together.`,
|
|
1208
|
+
"CONFLICTING_OPTIONS",
|
|
1209
|
+
2
|
|
1210
|
+
);
|
|
1211
|
+
if (options.demo && [
|
|
1212
|
+
options.organization,
|
|
1213
|
+
options.project,
|
|
1214
|
+
options.newOrganization,
|
|
1215
|
+
options.newProject,
|
|
1216
|
+
options.locales
|
|
1217
|
+
].some(Boolean))
|
|
1218
|
+
throw new CliError(
|
|
1219
|
+
"Project provisioning options cannot be combined with --demo.",
|
|
1220
|
+
"CONFLICTING_OPTIONS",
|
|
1221
|
+
2
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
function packageManager(provided) {
|
|
1225
|
+
const value = provided ?? /^(npm|pnpm|yarn|bun)\//.exec(
|
|
1226
|
+
process.env.npm_config_user_agent ?? ""
|
|
1227
|
+
)?.[1] ?? "npm";
|
|
1228
|
+
if (!["npm", "pnpm", "yarn", "bun"].includes(value))
|
|
1229
|
+
throw new CliError(
|
|
1230
|
+
"Package manager must be npm, pnpm, yarn, or bun.",
|
|
1231
|
+
"INVALID_PACKAGE_MANAGER",
|
|
1232
|
+
2
|
|
1233
|
+
);
|
|
1234
|
+
return value;
|
|
1235
|
+
}
|
|
1236
|
+
async function install(directory, manager) {
|
|
1237
|
+
await new Promise((done, reject) => {
|
|
1238
|
+
const child = spawn(
|
|
1239
|
+
process.platform === "win32" ? `${manager}.cmd` : manager,
|
|
1240
|
+
["install"],
|
|
1241
|
+
{
|
|
1242
|
+
cwd: directory,
|
|
1243
|
+
signal: cancellation.signal,
|
|
1244
|
+
stdio: [
|
|
1245
|
+
"ignore",
|
|
1246
|
+
process.argv.includes("--json") ? process.stderr : "inherit",
|
|
1247
|
+
"inherit"
|
|
1248
|
+
],
|
|
1249
|
+
shell: false
|
|
1250
|
+
}
|
|
1251
|
+
);
|
|
1252
|
+
child.once(
|
|
1253
|
+
"error",
|
|
1254
|
+
() => reject(
|
|
1255
|
+
new CliError(
|
|
1256
|
+
`Could not start ${manager}. Install it, then run ${manager} install in ${directory}.`,
|
|
1257
|
+
"INSTALL_FAILED"
|
|
1258
|
+
)
|
|
1259
|
+
)
|
|
1260
|
+
);
|
|
1261
|
+
child.once(
|
|
1262
|
+
"exit",
|
|
1263
|
+
(code) => code === 0 ? done() : reject(
|
|
1264
|
+
new CliError(
|
|
1265
|
+
`Dependency installation failed. Your app is saved in ${directory}; run ${manager} install there to retry.`,
|
|
1266
|
+
"INSTALL_FAILED"
|
|
1267
|
+
)
|
|
1268
|
+
)
|
|
1269
|
+
);
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
async function originFor(options) {
|
|
1273
|
+
return apiOrigin(
|
|
1274
|
+
options.apiUrl ?? (await readProject(process.cwd()))?.apiUrl ?? defaultApiUrl
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
projectOptions(
|
|
1278
|
+
program.command("create").description("Create a new app").command("app [directory]").option("--template <template>", "blog, saas, or storefront").option("--demo", "Use bundled translations without an account").option("--connect", "Connect a Linguana project").option("--package-manager <manager>", "npm, pnpm, yarn, or bun").option("--skip-install", "Write files without installing dependencies")
|
|
1279
|
+
).action(
|
|
1280
|
+
async (directory, _local, command) => {
|
|
1281
|
+
const options = opts(command);
|
|
1282
|
+
validateOptions(options);
|
|
1283
|
+
const ui = new Ui(options);
|
|
1284
|
+
const choices = {
|
|
1285
|
+
template: async () => options.template ?? await ui.choose(
|
|
1286
|
+
"Choose a template",
|
|
1287
|
+
templates.map((item) => ({
|
|
1288
|
+
value: item.id,
|
|
1289
|
+
label: `${item.name} \u2014 ${item.category}`
|
|
1290
|
+
}))
|
|
1291
|
+
),
|
|
1292
|
+
framework: async () => options.framework ?? await ui.choose(
|
|
1293
|
+
"Choose a framework",
|
|
1294
|
+
frameworks.map((value) => ({ value, label: frameworkLabels[value] }))
|
|
1295
|
+
)
|
|
1296
|
+
};
|
|
1297
|
+
const selectedChoices = ui.interactive ? await group(choices) : {
|
|
1298
|
+
template: await choices.template(),
|
|
1299
|
+
framework: await choices.framework()
|
|
1300
|
+
};
|
|
1301
|
+
const template = selectedChoices.template;
|
|
1302
|
+
if (!templateIds.includes(template))
|
|
1303
|
+
throw new CliError(
|
|
1304
|
+
"Template must be blog, saas, or storefront.",
|
|
1305
|
+
"INVALID_TEMPLATE",
|
|
1306
|
+
2
|
|
1307
|
+
);
|
|
1308
|
+
const selected = selectedChoices.framework;
|
|
1309
|
+
const framework = normalizeFramework(selected);
|
|
1310
|
+
if (!framework)
|
|
1311
|
+
throw new CliError("Unsupported framework.", "INVALID_FRAMEWORK", 2);
|
|
1312
|
+
const target = directory ?? await ui.input("Where should we create your app?", `my-${template}`);
|
|
1313
|
+
try {
|
|
1314
|
+
await validateDestination(target);
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
throw new CliError(
|
|
1317
|
+
error instanceof Error ? error.message : "Invalid destination.",
|
|
1318
|
+
"INVALID_DESTINATION",
|
|
1319
|
+
2
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
const manager = packageManager(
|
|
1323
|
+
options.packageManager ?? (ui.interactive ? await ui.choose(
|
|
1324
|
+
"Package manager",
|
|
1325
|
+
["npm", "pnpm", "yarn", "bun"].map((value) => ({
|
|
1326
|
+
value,
|
|
1327
|
+
label: value
|
|
1328
|
+
})),
|
|
1329
|
+
packageManager()
|
|
1330
|
+
) : void 0)
|
|
1331
|
+
);
|
|
1332
|
+
const mode = options.demo ? "demo" : options.connect ? "connected" : await ui.choose("Connect Linguana or try a demo?", [
|
|
1333
|
+
{
|
|
1334
|
+
value: "connected",
|
|
1335
|
+
label: "Connect Linguana",
|
|
1336
|
+
hint: "Browser sign-in and automatic project setup"
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
value: "demo",
|
|
1340
|
+
label: "Try the demo",
|
|
1341
|
+
hint: "No account required"
|
|
1342
|
+
}
|
|
1343
|
+
]);
|
|
1344
|
+
if (options.apiUrl) apiOrigin(options.apiUrl);
|
|
1345
|
+
const sourceRoot = fileURLToPath(new URL("./templates/", import.meta.url));
|
|
1346
|
+
const result = await ui.progress(
|
|
1347
|
+
"Create your app",
|
|
1348
|
+
() => scaffold({
|
|
1349
|
+
directory: target,
|
|
1350
|
+
framework,
|
|
1351
|
+
template,
|
|
1352
|
+
sourceRoot
|
|
1353
|
+
})
|
|
1354
|
+
);
|
|
1355
|
+
if (options.apiUrl) {
|
|
1356
|
+
const config = await readProject(result.directory);
|
|
1357
|
+
await writeJson(join5(result.directory, "linguana.json"), {
|
|
1358
|
+
...config,
|
|
1359
|
+
apiUrl: apiOrigin(options.apiUrl)
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
let connection;
|
|
1363
|
+
if (mode === "connected") {
|
|
1364
|
+
ui.notice(
|
|
1365
|
+
"Your app is saved. If setup is interrupted, run linguana link in its directory to resume."
|
|
1366
|
+
);
|
|
1367
|
+
connection = await connectProject(result.directory, options, ui);
|
|
1368
|
+
}
|
|
1369
|
+
if (!options.skipInstall)
|
|
1370
|
+
await ui.progress(
|
|
1371
|
+
"Install dependencies",
|
|
1372
|
+
() => install(result.directory, manager)
|
|
1373
|
+
);
|
|
1374
|
+
const quotedTarget = process.platform === "win32" ? JSON.stringify(target) : `'${target.replace(/'/g, "'\\''")}'`;
|
|
1375
|
+
ui.result(
|
|
1376
|
+
{
|
|
1377
|
+
directory: result.directory,
|
|
1378
|
+
framework,
|
|
1379
|
+
template,
|
|
1380
|
+
mode,
|
|
1381
|
+
...connection ? {
|
|
1382
|
+
projectId: connection.projectId,
|
|
1383
|
+
projectUrl: connection.projectUrl,
|
|
1384
|
+
envFile: connection.envFile
|
|
1385
|
+
} : {},
|
|
1386
|
+
nextCommand: `${manager} run dev`
|
|
1387
|
+
},
|
|
1388
|
+
`Your app is ready.
|
|
1389
|
+
|
|
1390
|
+
cd ${quotedTarget}
|
|
1391
|
+
${manager} run dev
|
|
1392
|
+
|
|
1393
|
+
Deployment: see DEPLOY.md in your app.${connection ? `
|
|
1394
|
+
Project: ${connection.projectUrl}
|
|
1395
|
+
Builds upload manifests. Paid translation and production publishing remain explicit actions in your dashboard.` : "\nConnect later with: npx @linguanahq/cli link"}`
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
);
|
|
1399
|
+
program.command("templates").command("list").action((_local, command) => {
|
|
1400
|
+
const ui = new Ui(opts(command));
|
|
1401
|
+
ui.result(
|
|
1402
|
+
{ templates, variants },
|
|
1403
|
+
templates.map(
|
|
1404
|
+
(item) => `${item.id.padEnd(12)} ${item.name} \u2014 ${item.description}
|
|
1405
|
+
${frameworks.join(", ")}${variants.filter(
|
|
1406
|
+
(variant) => variant.template === item.id && variant.demoUrl
|
|
1407
|
+
).map(
|
|
1408
|
+
(variant) => `
|
|
1409
|
+
${frameworkLabels[variant.framework]} demo: ${variant.demoUrl}`
|
|
1410
|
+
).join("")}`
|
|
1411
|
+
).join("\n\n")
|
|
1412
|
+
);
|
|
1413
|
+
});
|
|
1414
|
+
program.command("login").option(
|
|
1415
|
+
"--no-browser",
|
|
1416
|
+
"Print the authorization address without opening a browser"
|
|
1417
|
+
).action(async (_local, command) => {
|
|
1418
|
+
const options = opts(command);
|
|
1419
|
+
const ui = new Ui(options);
|
|
1420
|
+
const api = await login(await originFor(options), ui, {
|
|
1421
|
+
browser: options.browser
|
|
1422
|
+
});
|
|
1423
|
+
const session = await api.request("/api/auth/get-session");
|
|
1424
|
+
ui.result(
|
|
1425
|
+
{ user: session.user, origin: api.origin },
|
|
1426
|
+
`Signed in as ${session.user.email}.`
|
|
1427
|
+
);
|
|
1428
|
+
});
|
|
1429
|
+
program.command("logout").action(async (_local, command) => {
|
|
1430
|
+
const options = opts(command);
|
|
1431
|
+
const origin = await originFor(options);
|
|
1432
|
+
await logout(origin);
|
|
1433
|
+
new Ui(options).result({ signedOut: true, origin }, "Signed out of the CLI.");
|
|
1434
|
+
});
|
|
1435
|
+
program.command("whoami").action(async (_local, command) => {
|
|
1436
|
+
const options = opts(command);
|
|
1437
|
+
const ui = new Ui(options);
|
|
1438
|
+
const api = await authenticatedApi(await originFor(options), ui);
|
|
1439
|
+
const session = await api.request("/api/auth/get-session");
|
|
1440
|
+
ui.result(
|
|
1441
|
+
{ user: session.user, origin: api.origin },
|
|
1442
|
+
`${session.user.name} <${session.user.email}>
|
|
1443
|
+
${api.origin}`
|
|
1444
|
+
);
|
|
1445
|
+
});
|
|
1446
|
+
program.command("orgs").command("list").action(async (_local, command) => {
|
|
1447
|
+
const options = opts(command);
|
|
1448
|
+
const ui = new Ui(options);
|
|
1449
|
+
const api = await authenticatedApi(await originFor(options), ui);
|
|
1450
|
+
const result = await api.request(
|
|
1451
|
+
"/api/v1/organizations"
|
|
1452
|
+
);
|
|
1453
|
+
ui.result(
|
|
1454
|
+
result,
|
|
1455
|
+
result.map((item) => `${item.id} ${item.name}`).join("\n") || "No organizations. Use linguana link --new-organization <name>."
|
|
1456
|
+
);
|
|
1457
|
+
});
|
|
1458
|
+
program.command("projects").command("list").option("--organization <id>").action(async (_local, command) => {
|
|
1459
|
+
const options = opts(command);
|
|
1460
|
+
const ui = new Ui(options);
|
|
1461
|
+
const api = await authenticatedApi(await originFor(options), ui);
|
|
1462
|
+
const organizations = await api.request("/api/v1/organizations");
|
|
1463
|
+
const organization = options.organization ?? await ui.choose(
|
|
1464
|
+
"Choose an organization",
|
|
1465
|
+
organizations.map((item) => ({ value: item.id, label: item.name }))
|
|
1466
|
+
);
|
|
1467
|
+
const result = await api.request(
|
|
1468
|
+
`/api/v1/organizations/${encodeURIComponent(organization)}/projects`
|
|
1469
|
+
);
|
|
1470
|
+
ui.result(
|
|
1471
|
+
result,
|
|
1472
|
+
result.map((item) => `${item.id} ${item.name}`).join("\n") || "No projects. Use linguana link --new-project <name>."
|
|
1473
|
+
);
|
|
1474
|
+
});
|
|
1475
|
+
projectOptions(
|
|
1476
|
+
program.command("link").description("Connect this app and write environment configuration")
|
|
1477
|
+
).action(async (_local, command) => {
|
|
1478
|
+
const options = opts(command);
|
|
1479
|
+
validateOptions(options);
|
|
1480
|
+
const ui = new Ui(options);
|
|
1481
|
+
const result = await connectProject(process.cwd(), options, ui);
|
|
1482
|
+
const integration = result.config.framework === "tanstack-start" ? "getting-started/tanstack-start" : `reference/${result.config.framework === "solidjs" ? "solid" : result.config.framework}`;
|
|
1483
|
+
const provider = result.config.framework === "vue" ? "Register createLinguana(options) on your Vue app." : result.config.framework === "tanstack-start" ? "Load the request locale and catalogs on the server, then pass matching hydrationState to LocaleProvider." : result.config.framework === "svelte" ? "Create a client with createLinguana(options), call setLinguana(client) in your root component, and dispose it onDestroy." : "Wrap your app with LocaleProvider and pass the public project configuration.";
|
|
1484
|
+
ui.result(
|
|
1485
|
+
result,
|
|
1486
|
+
`Connected ${result.projectId}. Wrote ${result.envFile}.
|
|
1487
|
+
Project: ${result.projectUrl}
|
|
1488
|
+
${frameworkLabels[result.config.framework]} integration: https://linguana-docs.mohammedibrahim.dev/docs/${integration}
|
|
1489
|
+
Add the Linguana Vite adapter before your framework plugin and load the environment with loadEnv. ${provider} See the integration guide for exact imports.`
|
|
1490
|
+
);
|
|
1491
|
+
});
|
|
1492
|
+
projectOptions(
|
|
1493
|
+
program.command("env").command("pull").description("Refresh this app's environment credentials")
|
|
1494
|
+
).action(async (_local, command) => {
|
|
1495
|
+
const options = opts(command);
|
|
1496
|
+
validateOptions(options);
|
|
1497
|
+
const ui = new Ui(options);
|
|
1498
|
+
const config = await readProject(process.cwd());
|
|
1499
|
+
if (!config?.projectId)
|
|
1500
|
+
throw new CliError(
|
|
1501
|
+
"Run linguana link before pulling environment configuration.",
|
|
1502
|
+
"PROJECT_NOT_LINKED",
|
|
1503
|
+
2
|
|
1504
|
+
);
|
|
1505
|
+
const result = await connectProject(
|
|
1506
|
+
process.cwd(),
|
|
1507
|
+
{ ...options, project: config.projectId },
|
|
1508
|
+
ui
|
|
1509
|
+
);
|
|
1510
|
+
ui.result(result, `Updated ${result.envFile} for ${result.environment}.`);
|
|
1511
|
+
});
|
|
1512
|
+
program.command("doctor").option("--environment <name>").action(async (_local, command) => {
|
|
1513
|
+
const options = opts(command);
|
|
1514
|
+
const result = await doctor(resolve4(process.cwd()), options.environment);
|
|
1515
|
+
new Ui(options).result(
|
|
1516
|
+
result,
|
|
1517
|
+
result.checks.map(
|
|
1518
|
+
(check) => `${check.ok ? "\u2713" : "\u2717"} ${check.check}: ${check.message}`
|
|
1519
|
+
).join("\n")
|
|
1520
|
+
);
|
|
1521
|
+
if (!result.ok) process.exitCode = 1;
|
|
1522
|
+
});
|
|
1523
|
+
try {
|
|
1524
|
+
await program.parseAsync();
|
|
1525
|
+
} catch (caught) {
|
|
1526
|
+
const error = cancellation.signal.aborted ? new CliError(
|
|
1527
|
+
"Canceled. Your saved app can be resumed with linguana link.",
|
|
1528
|
+
"CANCELED",
|
|
1529
|
+
130
|
|
1530
|
+
) : caught;
|
|
1531
|
+
if (error instanceof CommanderError && ["commander.helpDisplayed", "commander.version"].includes(error.code))
|
|
1532
|
+
process.exitCode = 0;
|
|
1533
|
+
else {
|
|
1534
|
+
const code = error instanceof CliError ? error.code : error instanceof CommanderError ? "INVALID_ARGUMENTS" : "COMMAND_FAILED";
|
|
1535
|
+
const message = safeMessage(error);
|
|
1536
|
+
if (process.argv.includes("--json"))
|
|
1537
|
+
process.stdout.write(`${JSON.stringify({ error: { code, message } })}
|
|
1538
|
+
`);
|
|
1539
|
+
else process.stderr.write(`Linguana: ${message}
|
|
1540
|
+
`);
|
|
1541
|
+
process.exitCode = error instanceof CliError ? error.exitCode : error instanceof CommanderError ? 2 : 1;
|
|
1542
|
+
}
|
|
1543
|
+
}
|