create-playcanvas 0.15.0 → 0.16.1

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.
Files changed (43) hide show
  1. package/README.md +69 -24
  2. package/dist/index.mjs +11 -11
  3. package/package.json +1 -1
  4. package/templates/_shared/scene-editor/AGENTS.md +8 -0
  5. package/templates/_shared/scene-editor/README.md +15 -2
  6. package/templates/_shared/scene-editor/src/scene-editor.ts +186 -36
  7. package/templates/engine/ar-placement/index.html +20 -0
  8. package/templates/engine/ar-placement/src/main.ts +22 -12
  9. package/templates/engine/base/AGENTS.md +1 -0
  10. package/templates/engine/base/README.md +1 -1
  11. package/templates/engine/first-person-controller/index.html +19 -0
  12. package/templates/engine/first-person-controller/src/main.ts +16 -11
  13. package/templates/engine/product-configurator/index.html +30 -0
  14. package/templates/engine/product-configurator/src/main.ts +33 -32
  15. package/templates/engine/scene-editor/index.html +33 -0
  16. package/templates/engine/scene-editor/src/main.ts +56 -47
  17. package/templates/engine/sprite-game/index.html +19 -0
  18. package/templates/engine/sprite-game/src/main.ts +19 -11
  19. package/templates/engine/third-person-controller/index.html +19 -0
  20. package/templates/engine/third-person-controller/src/main.ts +16 -11
  21. package/templates/engine/vr-starter/index.html +20 -0
  22. package/templates/engine/vr-starter/src/main.ts +22 -12
  23. package/templates/index.ts +1 -14
  24. package/templates/react/base/AGENTS.md +1 -0
  25. package/templates/react/scene-editor/src/App.tsx +73 -40
  26. package/templates/web-components/base/AGENTS.md +1 -0
  27. package/templates/web-components/scene-editor/index.html +32 -25
  28. package/templates/web-components/scene-editor/src/main.ts +33 -11
  29. package/templates/_shared/interactive-sphere/README.md +0 -7
  30. package/templates/_shared/interactive-sphere/public/environment-map.png +0 -0
  31. package/templates/_shared/interactive-sphere/src/counter.css +0 -19
  32. package/templates/_shared/physics-playground/README.md +0 -7
  33. package/templates/engine/interactive-sphere/src/App.ts +0 -182
  34. package/templates/engine/interactive-sphere/src/main.ts +0 -16
  35. package/templates/engine/interactive-sphere/src/utils.ts +0 -16
  36. package/templates/engine/physics-playground/src/main.ts +0 -131
  37. package/templates/react/interactive-sphere/src/App.tsx +0 -21
  38. package/templates/react/interactive-sphere/src/Scene.tsx +0 -70
  39. package/templates/react/physics-playground/src/App.tsx +0 -85
  40. package/templates/web-components/interactive-sphere/index.html +0 -36
  41. package/templates/web-components/interactive-sphere/src/main.ts +0 -76
  42. package/templates/web-components/physics-playground/index.html +0 -46
  43. package/templates/web-components/physics-playground/src/main.ts +0 -32
package/README.md CHANGED
@@ -7,11 +7,11 @@
7
7
  [![Reddit](https://img.shields.io/badge/Reddit-FF4500?style=flat&logo=reddit&logoColor=white&color=black)](https://www.reddit.com/r/PlayCanvas)
8
8
  [![X](https://img.shields.io/badge/X-000000?style=flat&logo=x&logoColor=white&color=black)](https://x.com/intent/follow?screen_name=playcanvas)
9
9
 
10
- | [Engine Manual](https://developer.playcanvas.com/user-manual/engine/) | [React Manual](https://developer.playcanvas.com/user-manual/react/) | [Examples](https://playcanvas.github.io/) | [Forum](https://forum.playcanvas.com/) |
10
+ | [Engine Manual](https://developer.playcanvas.com/user-manual/engine/) | [React Manual](https://developer.playcanvas.com/user-manual/react/) | [Web Components Manual](https://developer.playcanvas.com/user-manual/web-components/) | [Examples](https://playcanvas.com/examples) | [Forum](https://forum.playcanvas.com/) |
11
11
 
12
12
  Scaffold a Vite-powered PlayCanvas project with TypeScript. Pick a format and a runnable starter, then build from there.
13
13
 
14
- ## Getting Started
14
+ ## Getting started
15
15
 
16
16
  ```bash
17
17
  # npm
@@ -28,17 +28,31 @@ Then follow the prompts. Requires Node.js 22.23.2 or later.
28
28
 
29
29
  ## Formats
30
30
 
31
- | Format | Description |
32
- | ---------------- | -------------------------------------------- |
33
- | `engine` | The PlayCanvas Engine API directly |
34
- | `react` | `@playcanvas/react` components |
35
- | `web-components` | `@playcanvas/web-components` custom elements |
31
+ | Format | Description |
32
+ | ---------------------------------------------------------------- | -------------------------------------------- |
33
+ | [`engine`](https://github.com/playcanvas/engine) | The PlayCanvas Engine API directly |
34
+ | [`react`](https://github.com/playcanvas/react) | `@playcanvas/react` components |
35
+ | [`web-components`](https://github.com/playcanvas/web-components) | `@playcanvas/web-components` custom elements |
36
36
 
37
- Every format uses TypeScript and includes Vite, ESLint, Prettier and a production build.
37
+ Every format uses TypeScript and includes Vite with hot reload, ESLint and Prettier. The development server loads the [PlayCanvas Inspector](https://github.com/playcanvas/inspector), a debug panel for the running scene, and production builds use relative paths, so they run from any folder, such as a GitHub Pages project site or an itch.io upload.
38
38
 
39
39
  ## Starters
40
40
 
41
- Choose from starter kits across basics, viewers, games, tools and XR. Run the creator to browse them, or pass `--help` to list them; `spinning-cube` is the default.
41
+ Every starter is a runnable scene, available in every format. Run the creator to browse them, or pass `--help` to list them; `spinning-cube` is the default.
42
+
43
+ | Starter | Category | Description |
44
+ | ------------------------- | -------- | --------------------------------------------------------------- |
45
+ | `spinning-cube` | Basics | A rotating cube and a light |
46
+ | `model-viewer` | Viewers | A glTF model, orbit controls and a procedural sky |
47
+ | `splat-viewer` | Viewers | A Gaussian splat and orbit controls |
48
+ | `product-configurator` | Viewers | Product variants, materials and orbit controls |
49
+ | `first-person-controller` | Games | First-person movement, mouse look and jumping |
50
+ | `third-person-controller` | Games | An animated character, follow camera and locomotion |
51
+ | `sprite-game` | Games | An orthographic camera, sprite animation and input |
52
+ | `web-lens` | Games | A portrait game with Snapchat leaderboards, packaged for upload |
53
+ | `scene-editor` | Tools | Click to select, gizmos and a scene saved in the browser |
54
+ | `vr-starter` | XR | Immersive VR, controllers and locomotion |
55
+ | `ar-placement` | XR | Mobile AR, hit testing and object placement |
42
56
 
43
57
  ![The PlayCanvas starter scenes](images/starter-catalog.webp)
44
58
 
@@ -52,7 +66,7 @@ npm create playcanvas@latest my-lens -- --starter web-lens
52
66
 
53
67
  Its `npm run package` script builds the game, checks it against the Web Lens upload rules (size limits, relative paths, no network access and no camera or microphone) and writes the zip to upload through [My Lenses](https://my-lenses.snapchat.com/).
54
68
 
55
- ## CLI Options
69
+ ## CLI options
56
70
 
57
71
  Pass a project name and options to skip the prompts:
58
72
 
@@ -60,35 +74,57 @@ Pass a project name and options to skip the prompts:
60
74
  npm create playcanvas@latest my-game -- -f react -y
61
75
  ```
62
76
 
63
- | Option | Shorthand | Description |
64
- | ------------------ | --------- | ------------------------------------------------------- |
65
- | `--format <name>` | `-f` | Use a format from the list above |
66
- | `--starter <name>` | `-s` | Use a starter from the list above |
67
- | `--overwrite` | | Remove existing files from a non-empty target directory |
68
- | `--keep` | | Add the project to a non-empty target directory |
69
- | `--no-skills` | | Omit the PlayCanvas agent skills (included by default) |
70
- | `--yes` | `-y` | Skip the prompts and take the defaults |
71
- | `--help` | `-h` | Show command help, with the formats and starters |
77
+ npm needs the `--` before the options. pnpm, yarn and bun take them without it, and may ignore them after one:
72
78
 
73
- `--yes` takes `playcanvas-project`, the `engine` format and the `spinning-cube` starter for anything you don't pass. It never touches files that are already there, so a non-empty target directory still needs `--keep` or `--overwrite`. A run without a terminal, such as one started by an AI agent or in CI, takes the same defaults, so it never waits on a prompt. The previous `--template` and `--boilerplate` long flags remain accepted for compatibility.
79
+ ```bash
80
+ pnpm create playcanvas@latest my-game -f react -y
81
+ ```
82
+
83
+ Use `.` as the project name to create the project in the current directory.
84
+
85
+ | Option | Shorthand | Description |
86
+ | ------------------ | --------- | ----------------------------------------------------------------------- |
87
+ | `--format <name>` | `-f` | Use a format from the list above |
88
+ | `--starter <name>` | `-s` | Use a starter from the list above |
89
+ | `--overwrite` | | Remove existing files, except `.git`, from a non-empty target directory |
90
+ | `--keep` | | Add the project to a non-empty target directory |
91
+ | `--no-skills` | | Omit the PlayCanvas agent skills (included by default) |
92
+ | `--yes` | `-y` | Skip the prompts and take the defaults |
93
+ | `--help` | `-h` | Show command help, with the formats and starters |
94
+
95
+ `--yes` takes `playcanvas-project`, the `engine` format and the `spinning-cube` starter for anything you don't pass. It never touches files that are already there, so a non-empty target directory still needs `--keep` or `--overwrite`. A run without a terminal, such as one started by an AI agent or in CI, takes the same defaults, so it never waits on a prompt. The previous `--template` (`-t`) and `--boilerplate` (`-b`) flags remain accepted for compatibility.
74
96
 
75
- `--keep` adds the project to a directory that already holds files, such as a brief, assets or a README, and replaces none of them: where the project has a file of the same name, yours stays and the creator lists it. It stops at a directory with a `package.json`, which is a project already.
97
+ `--keep` adds the project to a directory that already holds files, such as a brief, assets or a README, and replaces none of them: where the project has a file of the same name, yours stays and the creator lists it. It refuses a directory that already has a `package.json`, since that is a project already. To add a project to the current directory:
98
+
99
+ ```bash
100
+ npm create playcanvas@latest . -- --keep
101
+ ```
76
102
 
77
103
  ## Coding agents
78
104
 
79
- Every project includes an `AGENTS.md` that gives AI coding agents such as Claude Code, Codex and Cursor its commands, layout and conventions, and how to check a change. Its `CLAUDE.md` imports it, since Claude Code skips `AGENTS.md` when a `CLAUDE.md` exists in the project or a folder above it.
105
+ Every project includes an `AGENTS.md` that gives AI coding agents such as Claude Code, Codex and Cursor the project's commands, layout and conventions, and how to check a change. The project's `CLAUDE.md` imports `AGENTS.md`, since Claude Code skips `AGENTS.md` when a `CLAUDE.md` exists in the working directory or a folder above it.
80
106
 
81
107
  Every project also includes [`@playcanvas/skills`](https://github.com/playcanvas/skills), so those agents pick up PlayCanvas-specific workflows automatically, with no install step. They are copied into `.claude/skills/` and `.agents/skills/`. Pass `--no-skills` to leave them out.
82
108
 
109
+ Coding agents: start from the [PlayCanvas documentation index](https://developer.playcanvas.com/llms.txt), which leads to a Markdown version of every guide and API page.
110
+
83
111
  ## Development
84
112
 
85
113
  ```bash
86
114
  npm install
87
- npm test
88
115
  npm run build
89
116
  node dist/index.mjs
90
117
  ```
91
118
 
119
+ `npm run dev` builds a stub that runs the creator straight from `src/`, so a change takes effect without a rebuild. Before opening a pull request, run the checks CI runs:
120
+
121
+ ```bash
122
+ npm test
123
+ npm run lint
124
+ npm run fmt
125
+ npm run typecheck
126
+ ```
127
+
92
128
  Contributions are welcome. Please open an issue before proposing a new format, starter or another substantial change.
93
129
 
94
130
  ### Adding a starter
@@ -101,7 +137,16 @@ A project is assembled from the layers under `templates/`, in this order. A file
101
137
  4. `_features/<feature>/<layer>/`: what each of the starter's `features` needs in that format, as mapped by `FEATURES` in `templates/index.ts`
102
138
  5. `<format>/<starter>/`: the starter's scene in that format
103
139
 
104
- To add a starter, list it in `STARTERS` in `templates/index.ts`, add `_shared/<starter>/README.md`, and add its scene to every format: `engine/<starter>/src/main.ts`, `react/<starter>/src/App.tsx`, and `web-components/<starter>/index.html` with `src/main.ts`. Import `./starter.css` for the shared page and panel styles, and override a base file only when the starter needs something different. `npm test` checks that the starter exists in every format, and CI scaffolds and builds it in each one.
140
+ To add a starter:
141
+
142
+ 1. List it in `STARTERS` in `templates/index.ts`, with the `name`, `display`, `category` and `description` that the prompts and `--help` show. The first entry is the default. A starter can also name the `features` it uses, the files and folders of `_common/` and the format bases to `omit`, and an `outro` to follow the commands in the closing message.
143
+ 2. Add `_shared/<starter>/README.md`, a `##` section that describes the starter and its controls.
144
+ 3. Add its scene to every format: `engine/<starter>/src/main.ts`, `react/<starter>/src/App.tsx`, and `web-components/<starter>/index.html` with `src/main.ts`. Import `./starter.css` for the shared page and panel styles, and override a base file only when the starter needs something different.
145
+ 4. Add it to the table under [Starters](#starters) and to `images/starter-catalog.webp`.
146
+
147
+ `npm test` checks that the starter exists in every format, and CI scaffolds it in each one, then checks its formatting, lints, typechecks and builds it.
148
+
149
+ Name a layer's `.gitignore` or `.npmrc` as `_gitignore` or `_npmrc`, since npm leaves those two out of a published package. The scaffolder renames them.
105
150
 
106
151
  Write commands in a README or `AGENTS.md` for npm, as `npm install`, `npm run <script>` and `npm run <script> -- <args>`. The scaffolder rewrites them for the package manager that created the project, and leaves out the `## Agent skills` section of both when run with `--no-skills`.
107
152
 
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import{styleText as u}from"node:util";import L,{stdin as Ue,stdout as ye}from"node:process";import Ge from"node:readline";import"node:tty";import y from"node:fs";import b from"node:path";import Ke from"node:os";import{fileURLToPath as He}from"node:url";const ze=(()=>{const e=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g;return t=>{let r=0;for(e.lastIndex=0;e.test(t);)r+=1;return t.length-r}})(),Je=e=>e===12288||e>=65281&&e<=65376||e>=65504&&e<=65510,Ye=e=>e===8987||e===9001||e>=12272&&e<=12287||e>=12289&&e<=12350||e>=12441&&e<=12543||e>=12549&&e<=12591||e>=12593&&e<=12686||e>=12688&&e<=12771||e>=12783&&e<=12830||e>=12832&&e<=12871||e>=12880&&e<=19903||e>=65040&&e<=65049||e>=65072&&e<=65106||e>=65108&&e<=65126||e>=65128&&e<=65131||e>=127488&&e<=127490||e>=127504&&e<=127547||e>=127552&&e<=127560||e>=131072&&e<=196605||e>=196608&&e<=262141,qe=/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y,Xe=/[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y,$e=/(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/yu,Qe=/\t{1,1000}/y,we=new RegExp("[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F\\u20E3?))*","yu"),Ze=/(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y,et=new RegExp("\\p{M}+","gu"),tt={limit:1/0,ellipsis:""},xe=(e,t={},r={})=>{const i=t.limit??1/0,n=t.ellipsis??"",o=t?.ellipsisWidth??(n?xe(n,tt,r).width:0),l=0,a=r.controlWidth??0,s=r.tabWidth??8,c=r.emojiWidth??2,d=2,h=r.regularWidth??1,m=r.wideWidth??d,f=[[Ze,h],[qe,l],[Xe,a],[Qe,s],[we,c],[$e,m]];let g=0,w=0,_=e.length,F=0,j=!1,M=_,O=Math.max(0,i-o),v=0,C=0,$=0,I=0;e:for(;;){if(C>v||w>=_&&w>g){const R=e.slice(v,C)||e.slice(g,w);F=0;for(const D of R.replaceAll(et,"")){const A=D.codePointAt(0)||0;if(Je(A)?I=d:Ye(A)?I=m:I=h,$+I>O&&(M=Math.min(M,Math.max(v,g)+F)),$+I>i){j=!0;break e}F+=D.length,$+=I}v=C=0}if(w>=_)break e;for(let R=0,D=f.length;R<D;R++){const[A,H]=f[R];if(A.lastIndex=w,A.test(e)){if(F=A===$e?ze(e.slice(w,A.lastIndex)):A===we?1:A.lastIndex-w,I=F*H,$+I>O&&(M=Math.min(M,w+Math.floor((O-$)/H))),$+I>i){j=!0;break e}$+=I,v=g,C=w,w=g=A.lastIndex;continue e}}w+=1}return{width:j?O:$,index:j?M:_,truncated:j,ellipsed:j&&i>=o}},rt={limit:1/0,ellipsis:"",ellipsisWidth:0},W=(e,t={})=>xe(e,rt,t).width,Z="\x1B",ve="\x9B",it=39,ce="\x07",Se="[",st="]",Ee="m",ue=`${st}8;;`,_e=new RegExp(`(?:\\${Se}(?<code>\\d+)m|\\${ue}(?<uri>.*)${ce})`,"y"),Ie=e=>{if(e>=30&&e<=37||e>=90&&e<=97)return 39;if(e>=40&&e<=47||e>=100&&e<=107)return 49;if(e===1||e===2)return 22;if(e===3)return 23;if(e===4)return 24;if(e===7)return 27;if(e===8)return 28;if(e===9)return 29;if(e===0)return 0},Fe=e=>`${Z}${Se}${e}${Ee}`,Ce=e=>`${Z}${ue}${e}${ce}`,he=(e,t,r)=>{const i=t[Symbol.iterator]();let n=!1,o=!1,l=e.at(-1),a=l===void 0?0:W(l),s=i.next(),c=i.next(),d=0;for(;!s.done;){const h=s.value,m=W(h);a+m<=r?e[e.length-1]+=h:(e.push(h),a=0),(h===Z||h===ve)&&(n=!0,o=t.startsWith(ue,d+1)),n?o?h===ce&&(n=!1,o=!1):h===Ee&&(n=!1):(a+=m,a===r&&!c.done&&(e.push(""),a=0)),s=c,c=i.next(),d+=h.length}l=e.at(-1),!a&&l!==void 0&&l.length&&e.length>1&&(e[e.length-2]+=e.pop())},nt=e=>{const t=e.split(" ");let r=t.length;for(;r&&!W(t[r-1]);)r--;return r===t.length?e:t.slice(0,r).join(" ")+t.slice(r).join("")},at=(e,t,r={})=>{if(r.trim!==!1&&e.trim()==="")return"";let i="",n,o;const l=e.split(" ");let a=[""],s=0;for(let h=0;h<l.length;h++){const m=l[h];if(r.trim!==!1){const g=a.at(-1)??"",w=g.trimStart();g.length!==w.length&&(a[a.length-1]=w,s=W(w))}h!==0&&(s>=t&&(r.wordWrap===!1||r.trim===!1)&&(a.push(""),s=0),(s||r.trim===!1)&&(a[a.length-1]+=" ",s++));const f=W(m);if(r.hard&&f>t){const g=t-s,w=1+Math.floor((f-g-1)/t);Math.floor((f-1)/t)<w&&a.push(""),he(a,m,t),s=W(a.at(-1)??"");continue}if(s+f>t&&s&&f){if(r.wordWrap===!1&&s<t){he(a,m,t),s=W(a.at(-1)??"");continue}a.push(""),s=0}if(s+f>t&&r.wordWrap===!1){he(a,m,t),s=W(a.at(-1)??"");continue}a[a.length-1]+=m,s+=f}r.trim!==!1&&(a=a.map(h=>nt(h)));const c=a.join(`
2
+ import{styleText as u}from"node:util";import B,{stdin as Ue,stdout as ye}from"node:process";import Ge from"node:readline";import"node:tty";import y from"node:fs";import b from"node:path";import Ke from"node:os";import{fileURLToPath as He}from"node:url";const ze=(()=>{const e=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g;return t=>{let r=0;for(e.lastIndex=0;e.test(t);)r+=1;return t.length-r}})(),Je=e=>e===12288||e>=65281&&e<=65376||e>=65504&&e<=65510,Ye=e=>e===8987||e===9001||e>=12272&&e<=12287||e>=12289&&e<=12350||e>=12441&&e<=12543||e>=12549&&e<=12591||e>=12593&&e<=12686||e>=12688&&e<=12771||e>=12783&&e<=12830||e>=12832&&e<=12871||e>=12880&&e<=19903||e>=65040&&e<=65049||e>=65072&&e<=65106||e>=65108&&e<=65126||e>=65128&&e<=65131||e>=127488&&e<=127490||e>=127504&&e<=127547||e>=127552&&e<=127560||e>=131072&&e<=196605||e>=196608&&e<=262141,qe=/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y,Xe=/[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y,$e=/(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/yu,Qe=/\t{1,1000}/y,we=new RegExp("[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F\\u20E3?))*","yu"),Ze=/(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y,et=new RegExp("\\p{M}+","gu"),tt={limit:1/0,ellipsis:""},xe=(e,t={},r={})=>{const i=t.limit??1/0,n=t.ellipsis??"",o=t?.ellipsisWidth??(n?xe(n,tt,r).width:0),l=0,a=r.controlWidth??0,s=r.tabWidth??8,c=r.emojiWidth??2,d=2,h=r.regularWidth??1,m=r.wideWidth??d,f=[[Ze,h],[qe,l],[Xe,a],[Qe,s],[we,c],[$e,m]];let g=0,w=0,_=e.length,F=0,j=!1,M=_,O=Math.max(0,i-o),v=0,C=0,$=0,I=0;e:for(;;){if(C>v||w>=_&&w>g){const R=e.slice(v,C)||e.slice(g,w);F=0;for(const D of R.replaceAll(et,"")){const A=D.codePointAt(0)||0;if(Je(A)?I=d:Ye(A)?I=m:I=h,$+I>O&&(M=Math.min(M,Math.max(v,g)+F)),$+I>i){j=!0;break e}F+=D.length,$+=I}v=C=0}if(w>=_)break e;for(let R=0,D=f.length;R<D;R++){const[A,H]=f[R];if(A.lastIndex=w,A.test(e)){if(F=A===$e?ze(e.slice(w,A.lastIndex)):A===we?1:A.lastIndex-w,I=F*H,$+I>O&&(M=Math.min(M,w+Math.floor((O-$)/H))),$+I>i){j=!0;break e}$+=I,v=g,C=w,w=g=A.lastIndex;continue e}}w+=1}return{width:j?O:$,index:j?M:_,truncated:j,ellipsed:j&&i>=o}},rt={limit:1/0,ellipsis:"",ellipsisWidth:0},W=(e,t={})=>xe(e,rt,t).width,Z="\x1B",ve="\x9B",it=39,ce="\x07",Se="[",st="]",Ee="m",ue=`${st}8;;`,_e=new RegExp(`(?:\\${Se}(?<code>\\d+)m|\\${ue}(?<uri>.*)${ce})`,"y"),Ie=e=>{if(e>=30&&e<=37||e>=90&&e<=97)return 39;if(e>=40&&e<=47||e>=100&&e<=107)return 49;if(e===1||e===2)return 22;if(e===3)return 23;if(e===4)return 24;if(e===7)return 27;if(e===8)return 28;if(e===9)return 29;if(e===0)return 0},Fe=e=>`${Z}${Se}${e}${Ee}`,Ce=e=>`${Z}${ue}${e}${ce}`,he=(e,t,r)=>{const i=t[Symbol.iterator]();let n=!1,o=!1,l=e.at(-1),a=l===void 0?0:W(l),s=i.next(),c=i.next(),d=0;for(;!s.done;){const h=s.value,m=W(h);a+m<=r?e[e.length-1]+=h:(e.push(h),a=0),(h===Z||h===ve)&&(n=!0,o=t.startsWith(ue,d+1)),n?o?h===ce&&(n=!1,o=!1):h===Ee&&(n=!1):(a+=m,a===r&&!c.done&&(e.push(""),a=0)),s=c,c=i.next(),d+=h.length}l=e.at(-1),!a&&l!==void 0&&l.length&&e.length>1&&(e[e.length-2]+=e.pop())},nt=e=>{const t=e.split(" ");let r=t.length;for(;r&&!W(t[r-1]);)r--;return r===t.length?e:t.slice(0,r).join(" ")+t.slice(r).join("")},at=(e,t,r={})=>{if(r.trim!==!1&&e.trim()==="")return"";let i="",n,o;const l=e.split(" ");let a=[""],s=0;for(let h=0;h<l.length;h++){const m=l[h];if(r.trim!==!1){const g=a.at(-1)??"",w=g.trimStart();g.length!==w.length&&(a[a.length-1]=w,s=W(w))}h!==0&&(s>=t&&(r.wordWrap===!1||r.trim===!1)&&(a.push(""),s=0),(s||r.trim===!1)&&(a[a.length-1]+=" ",s++));const f=W(m);if(r.hard&&f>t){const g=t-s,w=1+Math.floor((f-g-1)/t);Math.floor((f-1)/t)<w&&a.push(""),he(a,m,t),s=W(a.at(-1)??"");continue}if(s+f>t&&s&&f){if(r.wordWrap===!1&&s<t){he(a,m,t),s=W(a.at(-1)??"");continue}a.push(""),s=0}if(s+f>t&&r.wordWrap===!1){he(a,m,t),s=W(a.at(-1)??"");continue}a[a.length-1]+=m,s+=f}r.trim!==!1&&(a=a.map(h=>nt(h)));const c=a.join(`
3
3
  `);let d=!1;for(let h=0;h<c.length;h++){const m=c[h];if(i+=m,d)d=!1;else if(d=m>="\uD800"&&m<="\uDBFF",d)continue;if(m===Z||m===ve){_e.lastIndex=h+1;const g=_e.exec(c)?.groups;if(g?.code!==void 0){const w=Number.parseFloat(g.code);n=w===it?void 0:w}else g?.uri!==void 0&&(o=g.uri.length===0?void 0:g.uri)}if(c[h+1]===`
4
4
  `){o&&(i+=Ce(""));const f=n?Ie(n):void 0;n&&f&&(i+=Fe(f))}else m===`
5
5
  `&&(n&&Ie(n)&&(i+=Fe(n)),o&&(i+=Ce(o)))}return i},ot=/\r?\n/;function ee(e,t,r){return String(e).normalize().split(ot).map(i=>at(i,t,r)).join(`
@@ -12,9 +12,9 @@ import{styleText as u}from"node:util";import L,{stdin as Ue,stdout as ye}from"no
12
12
  `).length-1;this.output.write(V.move(-999,t*-1))}render(){const t=ee(this._render(this)??"",ie(this.output),{hard:!0,trim:!1});if(t!==this._prevFrame){if(this.state==="initial")this.output.write(V.hide);else{const r=ut(this._prevFrame,t),i=Te(this.output);if(this.restoreCursor(),r){const n=Math.max(0,r.numLinesAfter-i),o=Math.max(0,r.numLinesBefore-i);let l=r.lines.find(a=>a>=n);if(l===void 0){this._prevFrame=t;return}if(r.lines.length===1){this.output.write(V.move(0,l-o)),this.output.write(de.lines(1));const a=t.split(`
13
13
  `);this.output.write(a[l]),this._prevFrame=t,this.output.write(V.move(0,a.length-l-1));return}else if(r.lines.length>1){if(n<o)l=n;else{const s=l-o;s>0&&this.output.write(V.move(0,s))}this.output.write(de.down());const a=t.split(`
14
14
  `).slice(l);this.output.write(a.join(`
15
- `)),this._prevFrame=t;return}}this.output.write(de.down())}this.output.write(t),this.state==="initial"&&(this.state="active"),this._prevFrame=t}}},mt=class extends Re{options;cursor=0;get _selectedValue(){return this.options[this.cursor]}changeValue(){const t=this._selectedValue;this.value=t===void 0?void 0:t.value}constructor(t){super(t,!1),this.options=t.options;const r=this.options.findIndex(({value:n})=>n===t.initialValue),i=r===-1?0:r;this.cursor=this.options[i]?.disabled?te(i,1,this.options):i,this.changeValue(),this.on("cursor",n=>{switch(n){case"left":case"up":this.cursor=te(this.cursor,-1,this.options);break;case"down":case"right":this.cursor=te(this.cursor,1,this.options);break}this.changeValue()})}};class dt extends Re{get userInputWithCursor(){if(this.state==="submit")return this.userInput;const t=this.userInput;if(this.cursor>=t.length)return`${this.userInput}\u2588`;const r=t.slice(0,this.cursor),i=t.slice(this.cursor,this.cursor+1),n=t.slice(this.cursor+1);return`${r}${u("inverse",i)}${n}`}get cursor(){return this._cursor}constructor(t){super({...t,initialUserInput:t.initialUserInput??t.initialValue}),this.on("userInput",r=>{this._setValue(r)}),this.on("finalize",()=>{this.value||(this.value=t.defaultValue),this.value===void 0&&(this.value="")})}}function pt(){return L.platform!=="win32"?L.env.TERM!=="linux":!!L.env.CI||!!L.env.WT_SESSION||!!L.env.TERMINUS_SUBLIME||L.env.ConEmuTask==="{cmd::Cmder}"||L.env.TERM_PROGRAM==="Terminus-Sublime"||L.env.TERM_PROGRAM==="vscode"||L.env.TERM==="xterm-256color"||L.env.TERM==="alacritty"||L.env.TERMINAL_EMULATOR==="JetBrains-JediTerm"}const ft=pt(),P=(e,t)=>ft?e:t,je=P("\u25C6","*"),gt=P("\u25A0","x"),bt=P("\u25B2","x"),Ae=P("\u25C7","o"),E=P("\u2502","|"),G=P("\u2514","\u2014"),yt=P("\u25CF",">"),Oe=P("\u25CB"," "),$t=P("\u25CF","\u2022"),wt=P("\u25C6","*"),xt=P("\u25B2","!"),vt=P("\u25A0","x"),Pe=e=>{switch(e){case"initial":case"active":return u("cyan",je);case"cancel":return u("red",gt);case"error":return u("yellow",bt);case"submit":return u("green",Ae);case"validating":return u("dim",je)}},St=e=>{switch(e){case"initial":case"active":return u("cyan",E);case"cancel":return u("red",E);case"error":return u("yellow",E);case"submit":return u("green",E)}};function Et(e,t,r){const i=r?`${u("cyan",E)} `:"",n=se(e,t.join(" \u2022 "),i).split(`
16
- `);return r&&n.push(u("cyan",G)),n}const Be=(e,t,r,i,n,o=!1)=>{let l=t,a=0;if(o)for(let s=i-1;s>=r;s--){const c=e[s];if(c&&(l-=c.length),a++,l<=n)break}else for(let s=r;s<i;s++){const c=e[s];if(c&&(l-=c.length),a++,l<=n)break}return{lineCount:l,removals:a}},_t=({cursor:e,options:t,style:r,output:i=process.stdout,maxItems:n=Number.POSITIVE_INFINITY,columnPadding:o=0,rowPadding:l=4})=>{const a=ie(i)-o,s=Te(i),c=u("dim","..."),d=Math.max(s-l,0),h=Math.max(Math.min(n,d),5);let m=0;e>=h-3&&(m=Math.max(Math.min(e-h+3,t.length-h),0));let f=h<t.length&&m>0,g=h<t.length&&m+h<t.length;const w=Math.min(m+h,t.length),_=[];let F=0;f&&F++,g&&F++;const j=m+(f?1:0),M=w-(g?1:0);for(let v=j;v<M;v++){const C=t[v],$=C!==void 0?r(C,v===e):"",I=ee($,a,{hard:!0,trim:!1}).split(`
17
- `);_.push(I),F+=I.length}if(F>d){let v=0,C=0,$=F;const I=e-j;let R=d;const D=()=>Be(_,$,0,I,R),A=()=>Be(_,$,I+1,_.length,R,!0);f?({lineCount:$,removals:v}=D(),$>R&&(g||(R-=1),{lineCount:$,removals:C}=A())):(g||(R-=1),{lineCount:$,removals:C}=A(),$>R&&(R-=1,{lineCount:$,removals:v}=D())),v>0&&(f=!0,_.splice(0,v)),C>0&&(g=!0,_.splice(_.length-C,C))}const O=[];f&&O.push(c);for(const v of _)for(const C of v)O.push(C);return g&&O.push(c),O};`${u("dim","\u2191/\u2193")}`,`${u("dim","Space:")}`,`${u("dim","Enter:")}`;const N={message:(e=[],{symbol:t=u("gray",E),secondarySymbol:r=u("gray",E),output:i=process.stdout,spacing:n=1,withGuide:o}={})=>{const l=[],a=o??U.withGuide,s=a?r:"",c=a?`${t} `:"",d=a?`${r} `:"";for(let m=0;m<n;m++)l.push(s);const h=Array.isArray(e)?e:e.split(`
15
+ `)),this._prevFrame=t;return}}this.output.write(de.down())}this.output.write(t),this.state==="initial"&&(this.state="active"),this._prevFrame=t}}},mt=class extends Re{options;cursor=0;get _selectedValue(){return this.options[this.cursor]}changeValue(){const t=this._selectedValue;this.value=t===void 0?void 0:t.value}constructor(t){super(t,!1),this.options=t.options;const r=this.options.findIndex(({value:n})=>n===t.initialValue),i=r===-1?0:r;this.cursor=this.options[i]?.disabled?te(i,1,this.options):i,this.changeValue(),this.on("cursor",n=>{switch(n){case"left":case"up":this.cursor=te(this.cursor,-1,this.options);break;case"down":case"right":this.cursor=te(this.cursor,1,this.options);break}this.changeValue()})}};class dt extends Re{get userInputWithCursor(){if(this.state==="submit")return this.userInput;const t=this.userInput;if(this.cursor>=t.length)return`${this.userInput}\u2588`;const r=t.slice(0,this.cursor),i=t.slice(this.cursor,this.cursor+1),n=t.slice(this.cursor+1);return`${r}${u("inverse",i)}${n}`}get cursor(){return this._cursor}constructor(t){super({...t,initialUserInput:t.initialUserInput??t.initialValue}),this.on("userInput",r=>{this._setValue(r)}),this.on("finalize",()=>{this.value||(this.value=t.defaultValue),this.value===void 0&&(this.value="")})}}function pt(){return B.platform!=="win32"?B.env.TERM!=="linux":!!B.env.CI||!!B.env.WT_SESSION||!!B.env.TERMINUS_SUBLIME||B.env.ConEmuTask==="{cmd::Cmder}"||B.env.TERM_PROGRAM==="Terminus-Sublime"||B.env.TERM_PROGRAM==="vscode"||B.env.TERM==="xterm-256color"||B.env.TERM==="alacritty"||B.env.TERMINAL_EMULATOR==="JetBrains-JediTerm"}const ft=pt(),L=(e,t)=>ft?e:t,je=L("\u25C6","*"),gt=L("\u25A0","x"),bt=L("\u25B2","x"),Ae=L("\u25C7","o"),E=L("\u2502","|"),G=L("\u2514","\u2014"),yt=L("\u25CF",">"),Oe=L("\u25CB"," "),$t=L("\u25CF","\u2022"),wt=L("\u25C6","*"),xt=L("\u25B2","!"),vt=L("\u25A0","x"),Le=e=>{switch(e){case"initial":case"active":return u("cyan",je);case"cancel":return u("red",gt);case"error":return u("yellow",bt);case"submit":return u("green",Ae);case"validating":return u("dim",je)}},St=e=>{switch(e){case"initial":case"active":return u("cyan",E);case"cancel":return u("red",E);case"error":return u("yellow",E);case"submit":return u("green",E)}};function Et(e,t,r){const i=r?`${u("cyan",E)} `:"",n=se(e,t.join(" \u2022 "),i).split(`
16
+ `);return r&&n.push(u("cyan",G)),n}const Pe=(e,t,r,i,n,o=!1)=>{let l=t,a=0;if(o)for(let s=i-1;s>=r;s--){const c=e[s];if(c&&(l-=c.length),a++,l<=n)break}else for(let s=r;s<i;s++){const c=e[s];if(c&&(l-=c.length),a++,l<=n)break}return{lineCount:l,removals:a}},_t=({cursor:e,options:t,style:r,output:i=process.stdout,maxItems:n=Number.POSITIVE_INFINITY,columnPadding:o=0,rowPadding:l=4})=>{const a=ie(i)-o,s=Te(i),c=u("dim","..."),d=Math.max(s-l,0),h=Math.max(Math.min(n,d),5);let m=0;e>=h-3&&(m=Math.max(Math.min(e-h+3,t.length-h),0));let f=h<t.length&&m>0,g=h<t.length&&m+h<t.length;const w=Math.min(m+h,t.length),_=[];let F=0;f&&F++,g&&F++;const j=m+(f?1:0),M=w-(g?1:0);for(let v=j;v<M;v++){const C=t[v],$=C!==void 0?r(C,v===e):"",I=ee($,a,{hard:!0,trim:!1}).split(`
17
+ `);_.push(I),F+=I.length}if(F>d){let v=0,C=0,$=F;const I=e-j;let R=d;const D=()=>Pe(_,$,0,I,R),A=()=>Pe(_,$,I+1,_.length,R,!0);f?({lineCount:$,removals:v}=D(),$>R&&(g||(R-=1),{lineCount:$,removals:C}=A())):(g||(R-=1),{lineCount:$,removals:C}=A(),$>R&&(R-=1,{lineCount:$,removals:v}=D())),v>0&&(f=!0,_.splice(0,v)),C>0&&(g=!0,_.splice(_.length-C,C))}const O=[];f&&O.push(c);for(const v of _)for(const C of v)O.push(C);return g&&O.push(c),O};`${u("dim","\u2191/\u2193")}`,`${u("dim","Space:")}`,`${u("dim","Enter:")}`;const N={message:(e=[],{symbol:t=u("gray",E),secondarySymbol:r=u("gray",E),output:i=process.stdout,spacing:n=1,withGuide:o}={})=>{const l=[],a=o??U.withGuide,s=a?r:"",c=a?`${t} `:"",d=a?`${r} `:"";for(let m=0;m<n;m++)l.push(s);const h=Array.isArray(e)?e:e.split(`
18
18
  `);if(h.length>0){const[m,...f]=h;m.length>0?l.push(`${c}${m}`):l.push(a?t:"");for(const g of f)g.length>0?l.push(`${d}${g}`):l.push(a?r:"")}i.write(`${l.join(`
19
19
  `)}
20
20
  `)},info:(e,t)=>{N.message(e,{...t,symbol:u("blue",$t)})},success:(e,t)=>{N.message(e,{...t,symbol:u("green",wt)})},step:(e,t)=>{N.message(e,{...t,symbol:u("green",Ae)})},warn:(e,t)=>{N.message(e,{...t,symbol:u("yellow",xt)})},warning:(e,t)=>{N.warn(e,t)},error:(e,t)=>{N.message(e,{...t,symbol:u("red",vt)})}},It=(e="",t)=>{const r=process.stdout,i=`${u("gray",G)} `;r.write(`${i}${u("red",e)}
@@ -25,7 +25,7 @@ ${u("gray",G)} `;r.write(`${i}${e}
25
25
  `)},Ct=[`${u("dim","\u2191/\u2193")} to navigate`,`${u("dim","Enter:")} confirm`],ne=(e,t)=>e.includes(`
26
26
  `)?e.split(`
27
27
  `).map(r=>t(r)).join(`
28
- `):t(e),fe=e=>{const t=(i,n)=>{if(i===void 0)return"";const o=i.label??String(i.value);switch(n){case"disabled":return`${u("gray",Oe)} ${ne(o,l=>u("gray",l))}${i.hint?` ${u("dim",`(${i.hint??"disabled"})`)}`:""}`;case"selected":return`${ne(o,l=>u("dim",l))}`;case"active":return`${u("green",yt)} ${o}${i.hint?` ${u("dim",`(${i.hint})`)}`:""}`;case"cancelled":return`${ne(o,l=>u(["strikethrough","dim"],l))}`;default:return`${u("dim",Oe)} ${ne(o,l=>u("dim",l))}`}},r=e.showInstructions??!0;return new mt({options:e.options,signal:e.signal,input:e.input,output:e.output,initialValue:e.initialValue,render(){const i=e.withGuide??U.withGuide,n=`${Pe(this.state)} `,o=`${St(this.state)} `,l=se(e.output,e.message,o,n),a=`${i?`${u("gray",E)}
28
+ `):t(e),fe=e=>{const t=(i,n)=>{if(i===void 0)return"";const o=i.label??String(i.value);switch(n){case"disabled":return`${u("gray",Oe)} ${ne(o,l=>u("gray",l))}${i.hint?` ${u("dim",`(${i.hint??"disabled"})`)}`:""}`;case"selected":return`${ne(o,l=>u("dim",l))}`;case"active":return`${u("green",yt)} ${o}${i.hint?` ${u("dim",`(${i.hint})`)}`:""}`;case"cancelled":return`${ne(o,l=>u(["strikethrough","dim"],l))}`;default:return`${u("dim",Oe)} ${ne(o,l=>u("dim",l))}`}},r=e.showInstructions??!0;return new mt({options:e.options,signal:e.signal,input:e.input,output:e.output,initialValue:e.initialValue,render(){const i=e.withGuide??U.withGuide,n=`${Le(this.state)} `,o=`${St(this.state)} `,l=se(e.output,e.message,o,n),a=`${i?`${u("gray",E)}
29
29
  `:""}${l}
30
30
  `;switch(this.state){case"submit":{const s=i?`${u("gray",E)} `:"",c=se(e.output,t(this.options[this.cursor],"selected"),s);return`${a}${c}`}case"cancel":{const s=i?`${u("gray",E)} `:"",c=se(e.output,t(this.options[this.cursor],"cancelled"),s);return`${a}${c}${i?`
31
31
  ${u("gray",E)}`:""}`}default:{const s=i?`${u("cyan",E)} `:"",c=a.split(`
@@ -33,8 +33,8 @@ ${u("gray",E)}`:""}`}default:{const s=i?`${u("cyan",E)} `:"",c=a.split(`
33
33
  `),m=d.length+1;return`${a}${s}${_t({output:e.output,cursor:this.cursor,options:this.options,maxItems:e.maxItems,columnPadding:s.length,rowPadding:c+m,style:(f,g)=>t(f,f.disabled?"disabled":g?"active":"inactive")}).join(`
34
34
  ${s}`)}
35
35
  ${h}
36
- `}}}}).prompt()};`${u("gray",E)}`;const Le=e=>new dt({validate:e.validate,placeholder:e.placeholder,defaultValue:e.defaultValue,initialValue:e.initialValue,output:e.output,signal:e.signal,input:e.input,render(){const t=e?.withGuide??U.withGuide,r=`${`${t?`${u("gray",E)}
37
- `:""}${Pe(this.state)} `}${e.message}
36
+ `}}}}).prompt()};`${u("gray",E)}`;const Be=e=>new dt({validate:e.validate,placeholder:e.placeholder,defaultValue:e.defaultValue,initialValue:e.initialValue,output:e.output,signal:e.signal,input:e.input,render(){const t=e?.withGuide??U.withGuide,r=`${`${t?`${u("gray",E)}
37
+ `:""}${Le(this.state)} `}${e.message}
38
38
  `,i=e.placeholder&&e.placeholder.length>0?u("inverse",e.placeholder[0])+u("dim",e.placeholder.slice(1)):u(["inverse","hidden"],"_"),n=this.userInput?this.userInputWithCursor:i,o=this.value??"";switch(this.state){case"validating":{const l=t?`${u("cyan",E)} `:"",a=t?u("cyan",G):"",s=u("dim",n),c=u("dim","Validating...");return`${r}${l}${s}
39
39
  ${a} ${c}
40
40
  `}case"error":{const l=this.error?` ${u("yellow",this.error)}`:"",a=t?`${u("yellow",E)} `:"",s=t?u("yellow",G):"";return`${r.trim()}
@@ -43,11 +43,11 @@ ${s}${l}
43
43
  `}case"submit":{const l=o?`${t?" ":""}${u("dim",o)}`:"",a=t?u("gray",E):"";return`${r}${a}${l}`}case"cancel":{const l=o?` ${u(["strikethrough","dim"],o)}`:"",a=t?u("gray",E):"";return`${r}${a}${l}${o.trim()?`
44
44
  ${a}`:""}`}default:{const l=t?`${u("cyan",E)} `:"",a=t?u("cyan",G):"";return`${r}${l}${n}
45
45
  ${a}
46
- `}}}}).prompt();function ge(e){return e==null?[]:Array.isArray(e)?e:[e]}function kt(e,t,r,i){var n,o=e[t],l=~i.string.indexOf(t)?r==null||r===!0?"":String(r):typeof r=="boolean"?r:~i.boolean.indexOf(t)?r==="false"?!1:r==="true"||(e._.push((n=+r,n*0===0?n:r)),!!r):(n=+r,n*0===0?n:r);e[t]=o==null?l:Array.isArray(o)?o.concat(l):[o,l]}function Tt(e,t){e=e||[],t=t||{};var r,i,n,o,l,a={_:[]},s=0,c=0,d=0,h=e.length;const m=t.alias!==void 0,f=t.unknown!==void 0,g=t.default!==void 0;if(t.alias=t.alias||{},t.string=ge(t.string),t.boolean=ge(t.boolean),m)for(r in t.alias)for(i=t.alias[r]=ge(t.alias[r]),s=0;s<i.length;s++)(t.alias[i[s]]=i.concat(r)).splice(s,1);for(s=t.boolean.length;s-- >0;)for(i=t.alias[t.boolean[s]]||[],c=i.length;c-- >0;)t.boolean.push(i[c]);for(s=t.string.length;s-- >0;)for(i=t.alias[t.string[s]]||[],c=i.length;c-- >0;)t.string.push(i[c]);if(g){for(r in t.default)if(o=typeof t.default[r],i=t.alias[r]=t.alias[r]||[],t[o]!==void 0)for(t[o].push(r),s=0;s<i.length;s++)t[o].push(i[s])}const w=f?Object.keys(t.alias):[];for(s=0;s<h;s++){if(n=e[s],n==="--"){a._=a._.concat(e.slice(++s));break}for(c=0;c<n.length&&n.charCodeAt(c)===45;c++);if(c===0)a._.push(n);else if(n.substring(c,c+3)==="no-"){if(o=n.substring(c+3),f&&!~w.indexOf(o))return t.unknown(n);a[o]=!1}else{for(d=c+1;d<n.length&&n.charCodeAt(d)!==61;d++);for(o=n.substring(c,d),l=n.substring(++d)||s+1===h||(""+e[s+1]).charCodeAt(0)===45||e[++s],i=c===2?[o]:o,d=0;d<i.length;d++){if(o=i[d],f&&!~w.indexOf(o))return t.unknown("-".repeat(c)+o);kt(a,o,d+1<i.length||l,t)}}}if(g)for(r in t.default)a[r]===void 0&&(a[r]=t.default[r]);if(m)for(r in a)for(i=t.alias[r]||[];i.length>0;)a[i.shift()]=a[r];return a}function Rt(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ae={exports:{}},Me;function jt(){if(Me)return ae.exports;Me=1;let e=process||{},t=e.argv||[],r=e.env||{},i=!(r.NO_COLOR||t.includes("--no-color"))&&(!!r.FORCE_COLOR||t.includes("--color")||e.platform==="win32"||(e.stdout||{}).isTTY&&r.TERM!=="dumb"||!!r.CI),n=(a,s,c=a)=>d=>{let h=""+d,m=h.indexOf(s,a.length);return~m?a+o(h,s,c,m)+s:a+h+s},o=(a,s,c,d)=>{let h="",m=0;do h+=a.substring(m,d)+c,m=d+s.length,d=a.indexOf(s,m);while(~d);return h+a.substring(m)},l=(a=i)=>{let s=a?n:()=>String;return{isColorSupported:a,reset:s("\x1B[0m","\x1B[0m"),bold:s("\x1B[1m","\x1B[22m","\x1B[22m\x1B[1m"),dim:s("\x1B[2m","\x1B[22m","\x1B[22m\x1B[2m"),italic:s("\x1B[3m","\x1B[23m"),underline:s("\x1B[4m","\x1B[24m"),inverse:s("\x1B[7m","\x1B[27m"),hidden:s("\x1B[8m","\x1B[28m"),strikethrough:s("\x1B[9m","\x1B[29m"),black:s("\x1B[30m","\x1B[39m"),red:s("\x1B[31m","\x1B[39m"),green:s("\x1B[32m","\x1B[39m"),yellow:s("\x1B[33m","\x1B[39m"),blue:s("\x1B[34m","\x1B[39m"),magenta:s("\x1B[35m","\x1B[39m"),cyan:s("\x1B[36m","\x1B[39m"),white:s("\x1B[37m","\x1B[39m"),gray:s("\x1B[90m","\x1B[39m"),bgBlack:s("\x1B[40m","\x1B[49m"),bgRed:s("\x1B[41m","\x1B[49m"),bgGreen:s("\x1B[42m","\x1B[49m"),bgYellow:s("\x1B[43m","\x1B[49m"),bgBlue:s("\x1B[44m","\x1B[49m"),bgMagenta:s("\x1B[45m","\x1B[49m"),bgCyan:s("\x1B[46m","\x1B[49m"),bgWhite:s("\x1B[47m","\x1B[49m"),blackBright:s("\x1B[90m","\x1B[39m"),redBright:s("\x1B[91m","\x1B[39m"),greenBright:s("\x1B[92m","\x1B[39m"),yellowBright:s("\x1B[93m","\x1B[39m"),blueBright:s("\x1B[94m","\x1B[39m"),magentaBright:s("\x1B[95m","\x1B[39m"),cyanBright:s("\x1B[96m","\x1B[39m"),whiteBright:s("\x1B[97m","\x1B[39m"),bgBlackBright:s("\x1B[100m","\x1B[49m"),bgRedBright:s("\x1B[101m","\x1B[49m"),bgGreenBright:s("\x1B[102m","\x1B[49m"),bgYellowBright:s("\x1B[103m","\x1B[49m"),bgBlueBright:s("\x1B[104m","\x1B[49m"),bgMagentaBright:s("\x1B[105m","\x1B[49m"),bgCyanBright:s("\x1B[106m","\x1B[49m"),bgWhiteBright:s("\x1B[107m","\x1B[49m")}};return ae.exports=l(),ae.exports.createColors=l,ae.exports}var At=jt();const Ot=Rt(At),{cyan:Pt,magenta:Bt,yellow:Lt}=Ot,q=[{name:"engine",display:"Engine",description:"with the PlayCanvas Engine API directly",color:Lt},{name:"react",display:"React",description:"with @playcanvas/react",color:Pt},{name:"web-components",display:"Web Components",description:"with @playcanvas/web-components",color:Bt}],Mt={physics:{engine:["assets"],react:["react"],"web-components":["assets"]}},K=[{name:"spinning-cube",display:"Spinning Cube",category:"Basics",description:"a rotating cube and a light"},{name:"interactive-sphere",display:"Interactive Sphere",category:"Basics",description:"orbit controls, a grid and pointer picking"},{name:"model-viewer",display:"Model Viewer",category:"Viewers",description:"a glTF model, orbit controls and a procedural sky"},{name:"splat-viewer",display:"Splat Viewer",category:"Viewers",description:"a Gaussian splat and orbit controls"},{name:"product-configurator",display:"Product Configurator",category:"Viewers",description:"product variants, materials and orbit controls"},{name:"physics-playground",display:"Physics Playground",category:"Games",description:"rigid bodies, collisions, spawning and reset",features:["physics"]},{name:"first-person-controller",display:"First-Person Controller",category:"Games",description:"first-person movement, mouse look and jumping",features:["physics"]},{name:"third-person-controller",display:"Third-Person Controller",category:"Games",description:"an animated character, follow camera and locomotion",features:["physics"]},{name:"sprite-game",display:"Sprite Game",category:"Games",description:"an orthographic camera, sprite animation and input"},{name:"web-lens",display:"Snapchat Web Lens",category:"Games",description:"a portrait game with Snapchat leaderboards, packaged for upload",omit:["src/starter.css","src/assets"],outro:`To publish it to Snapchat, run:
46
+ `}}}}).prompt();function ge(e){return e==null?[]:Array.isArray(e)?e:[e]}function kt(e,t,r,i){var n,o=e[t],l=~i.string.indexOf(t)?r==null||r===!0?"":String(r):typeof r=="boolean"?r:~i.boolean.indexOf(t)?r==="false"?!1:r==="true"||(e._.push((n=+r,n*0===0?n:r)),!!r):(n=+r,n*0===0?n:r);e[t]=o==null?l:Array.isArray(o)?o.concat(l):[o,l]}function Tt(e,t){e=e||[],t=t||{};var r,i,n,o,l,a={_:[]},s=0,c=0,d=0,h=e.length;const m=t.alias!==void 0,f=t.unknown!==void 0,g=t.default!==void 0;if(t.alias=t.alias||{},t.string=ge(t.string),t.boolean=ge(t.boolean),m)for(r in t.alias)for(i=t.alias[r]=ge(t.alias[r]),s=0;s<i.length;s++)(t.alias[i[s]]=i.concat(r)).splice(s,1);for(s=t.boolean.length;s-- >0;)for(i=t.alias[t.boolean[s]]||[],c=i.length;c-- >0;)t.boolean.push(i[c]);for(s=t.string.length;s-- >0;)for(i=t.alias[t.string[s]]||[],c=i.length;c-- >0;)t.string.push(i[c]);if(g){for(r in t.default)if(o=typeof t.default[r],i=t.alias[r]=t.alias[r]||[],t[o]!==void 0)for(t[o].push(r),s=0;s<i.length;s++)t[o].push(i[s])}const w=f?Object.keys(t.alias):[];for(s=0;s<h;s++){if(n=e[s],n==="--"){a._=a._.concat(e.slice(++s));break}for(c=0;c<n.length&&n.charCodeAt(c)===45;c++);if(c===0)a._.push(n);else if(n.substring(c,c+3)==="no-"){if(o=n.substring(c+3),f&&!~w.indexOf(o))return t.unknown(n);a[o]=!1}else{for(d=c+1;d<n.length&&n.charCodeAt(d)!==61;d++);for(o=n.substring(c,d),l=n.substring(++d)||s+1===h||(""+e[s+1]).charCodeAt(0)===45||e[++s],i=c===2?[o]:o,d=0;d<i.length;d++){if(o=i[d],f&&!~w.indexOf(o))return t.unknown("-".repeat(c)+o);kt(a,o,d+1<i.length||l,t)}}}if(g)for(r in t.default)a[r]===void 0&&(a[r]=t.default[r]);if(m)for(r in a)for(i=t.alias[r]||[];i.length>0;)a[i.shift()]=a[r];return a}function Rt(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var ae={exports:{}},Me;function jt(){if(Me)return ae.exports;Me=1;let e=process||{},t=e.argv||[],r=e.env||{},i=!(r.NO_COLOR||t.includes("--no-color"))&&(!!r.FORCE_COLOR||t.includes("--color")||e.platform==="win32"||(e.stdout||{}).isTTY&&r.TERM!=="dumb"||!!r.CI),n=(a,s,c=a)=>d=>{let h=""+d,m=h.indexOf(s,a.length);return~m?a+o(h,s,c,m)+s:a+h+s},o=(a,s,c,d)=>{let h="",m=0;do h+=a.substring(m,d)+c,m=d+s.length,d=a.indexOf(s,m);while(~d);return h+a.substring(m)},l=(a=i)=>{let s=a?n:()=>String;return{isColorSupported:a,reset:s("\x1B[0m","\x1B[0m"),bold:s("\x1B[1m","\x1B[22m","\x1B[22m\x1B[1m"),dim:s("\x1B[2m","\x1B[22m","\x1B[22m\x1B[2m"),italic:s("\x1B[3m","\x1B[23m"),underline:s("\x1B[4m","\x1B[24m"),inverse:s("\x1B[7m","\x1B[27m"),hidden:s("\x1B[8m","\x1B[28m"),strikethrough:s("\x1B[9m","\x1B[29m"),black:s("\x1B[30m","\x1B[39m"),red:s("\x1B[31m","\x1B[39m"),green:s("\x1B[32m","\x1B[39m"),yellow:s("\x1B[33m","\x1B[39m"),blue:s("\x1B[34m","\x1B[39m"),magenta:s("\x1B[35m","\x1B[39m"),cyan:s("\x1B[36m","\x1B[39m"),white:s("\x1B[37m","\x1B[39m"),gray:s("\x1B[90m","\x1B[39m"),bgBlack:s("\x1B[40m","\x1B[49m"),bgRed:s("\x1B[41m","\x1B[49m"),bgGreen:s("\x1B[42m","\x1B[49m"),bgYellow:s("\x1B[43m","\x1B[49m"),bgBlue:s("\x1B[44m","\x1B[49m"),bgMagenta:s("\x1B[45m","\x1B[49m"),bgCyan:s("\x1B[46m","\x1B[49m"),bgWhite:s("\x1B[47m","\x1B[49m"),blackBright:s("\x1B[90m","\x1B[39m"),redBright:s("\x1B[91m","\x1B[39m"),greenBright:s("\x1B[92m","\x1B[39m"),yellowBright:s("\x1B[93m","\x1B[39m"),blueBright:s("\x1B[94m","\x1B[39m"),magentaBright:s("\x1B[95m","\x1B[39m"),cyanBright:s("\x1B[96m","\x1B[39m"),whiteBright:s("\x1B[97m","\x1B[39m"),bgBlackBright:s("\x1B[100m","\x1B[49m"),bgRedBright:s("\x1B[101m","\x1B[49m"),bgGreenBright:s("\x1B[102m","\x1B[49m"),bgYellowBright:s("\x1B[103m","\x1B[49m"),bgBlueBright:s("\x1B[104m","\x1B[49m"),bgMagentaBright:s("\x1B[105m","\x1B[49m"),bgCyanBright:s("\x1B[106m","\x1B[49m"),bgWhiteBright:s("\x1B[107m","\x1B[49m")}};return ae.exports=l(),ae.exports.createColors=l,ae.exports}var At=jt();const Ot=Rt(At),{cyan:Lt,magenta:Pt,yellow:Bt}=Ot,q=[{name:"engine",display:"Engine",description:"with the PlayCanvas Engine API directly",color:Bt},{name:"react",display:"React",description:"with @playcanvas/react",color:Lt},{name:"web-components",display:"Web Components",description:"with @playcanvas/web-components",color:Pt}],Mt={physics:{engine:["assets"],react:["react"],"web-components":["assets"]}},K=[{name:"spinning-cube",display:"Spinning Cube",category:"Basics",description:"a rotating cube and a light"},{name:"model-viewer",display:"Model Viewer",category:"Viewers",description:"a glTF model, orbit controls and a procedural sky"},{name:"splat-viewer",display:"Splat Viewer",category:"Viewers",description:"a Gaussian splat and orbit controls"},{name:"product-configurator",display:"Product Configurator",category:"Viewers",description:"product variants, materials and orbit controls"},{name:"first-person-controller",display:"First-Person Controller",category:"Games",description:"first-person movement, mouse look and jumping",features:["physics"]},{name:"third-person-controller",display:"Third-Person Controller",category:"Games",description:"an animated character, follow camera and locomotion",features:["physics"]},{name:"sprite-game",display:"Sprite Game",category:"Games",description:"an orthographic camera, sprite animation and input"},{name:"web-lens",display:"Snapchat Web Lens",category:"Games",description:"a portrait game with Snapchat leaderboards, packaged for upload",omit:["src/starter.css","src/assets"],outro:`To publish it to Snapchat, run:
47
47
 
48
48
  npm run package
49
49
 
50
- and upload the zip it writes to dist/ at https://my-lenses.snapchat.com/`},{name:"scene-editor",display:"Scene Editor",category:"Tools",description:"selection and translate, rotate and scale gizmos"},{name:"vr-starter",display:"VR Starter",category:"XR",description:"immersive VR, controllers and locomotion"},{name:"ar-placement",display:"AR Placement",category:"XR",description:"mobile AR, hit testing and object placement"}],Nt=async({argFormat:e,yes:t,cancel:r})=>{if(e&&q.some(n=>n.name===e))return e;if(e&&t)throw new Error(`Unknown format: ${e}. Choose from: ${q.map(n=>n.name).join(", ")}`);if(t)return q[0].name;const i=await fe({message:e?`"${e}" isn't a valid format. Please choose from below: `:"Select a format:",options:q.map(n=>({label:n.color(n.display),value:n.name,hint:n.description}))});return Y(i)&&r(),i},Dt=async({argStarter:e,yes:t,cancel:r})=>{if(e&&K.some(n=>n.name===e))return e;if(t){if(!e)return K[0].name;throw new Error(`Unknown starter: ${e}. Choose from: ${K.map(n=>n.name).join(", ")}`)}const i=await fe({message:e?`"${e}" isn't a valid starter. Please choose from below: `:"Select a starter:",initialValue:K[0].name,options:K.map(n=>({label:n.display,value:n.name,hint:`${n.category} \xB7 ${n.description}`}))});return Y(i)&&r(),i},Ne=e=>/^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(e),be=e=>e.trim().toLowerCase().replace(/\s+/g,"-").replace(/^[._]/,"").replace(/[^a-z\d\-~]+/g,"-"),Wt=async({targetDir:e,yes:t,cancel:r})=>{let i=b.basename(b.resolve(e));if(!Ne(i)){if(t)return be(i);const n=await Le({message:"Package name:",defaultValue:be(i),placeholder:be(i),validate:o=>{if(!o||!Ne(o))return"Invalid package.json name"}});if(Y(n))return r();i=n}return i},oe=e=>e.trim().replace(/\/+$/g,""),Vt=e=>{const t=y.readdirSync(e);return t.length===0||t.length===1&&t[0]===".git"},Ut=e=>{if(y.existsSync(e))for(const t of y.readdirSync(e))t!==".git"&&y.rmSync(b.resolve(e,t),{recursive:!0,force:!0})},Gt=async({argTargetDir:e,defaultTargetDir:t,yes:r,cancel:i})=>{let n=e?oe(String(e)):void 0;if(!n&&r)return t;if(!n){const o=await Le({message:"Project name:",defaultValue:t,placeholder:t,validate:l=>{if(l&&!oe(l))return"Invalid project name"}});Y(o)&&i(),n=oe(o)}return n},Kt=async({targetDir:e,argOverwrite:t,argKeep:r,yes:i,cancel:n})=>{if(t&&r)throw new Error("Pass --keep or --overwrite, not both.");if(!y.existsSync(e)||Vt(e))return!1;let o="no";if(t)o="yes";else if(r)o="keep";else{if(i)throw new Error(`Target directory "${e}" is not empty. Pass --keep to add the project to its files, or --overwrite to remove them.`);o=await fe({message:(e==="."?"Current directory":`Target directory "${e}"`)+" is not empty. Please choose how to proceed:",options:[{label:"Cancel operation",value:"no"},{label:"Remove existing files and continue",value:"yes"},{label:"Keep existing files and continue",value:"keep"}]})}if(Y(o))return n();switch(o){case"yes":return Ut(e),!1;case"keep":if(y.existsSync(b.join(e,"package.json")))throw new Error(`Target directory "${e}" already has a package.json, so it can't keep its files. Pass --overwrite to remove them.`);return!0;default:return n()}},Ht=["scripts","dependencies","devDependencies","peerDependencies","engines"],De={"README.md":"## Prerequisites","AGENTS.md":"## Checking your work"},zt="## Agent skills",Jt=["npm","pnpm","yarn","bun"],Yt=e=>e==="yarn"?{install:"yarn",run:"yarn"}:{install:`${e} install`,run:`${e} run`},qt=e=>{const{cwd:t,targetDir:r,format:i,starter:n,packageName:o,skills:l,keep:a,renameFiles:s}=e,c=b.resolve(t,r);y.mkdirSync(c,{recursive:!0});const d=process.env.npm_config_user_agent?.split("/")[0]??"npm",h=Jt.includes(d)?d:"npm",{install:m,run:f}=Yt(h),g=p=>h==="npm"?p:p.replace(/\bnpm run ([^\s`]+) -- /g,`${f} $1 `).replace(/\bnpm run\b/g,f).replace(/\bnpm install\b/g,m);N.step(`Creating project in ${c}...`);const w=He(import.meta.url),_=b.dirname(w),F=e.templatesDir??[b.resolve(_,"../templates"),b.resolve(_,"../../templates")].find(p=>y.existsSync(b.join(p,i)));if(!F)throw new Error(`Format directory not found for: ${i}`);const j=K.find(p=>p.name===n);if(!j)throw new Error(`Starter not found: ${n}`);const M=b.join(F,"_common"),O=b.join(F,i,"base"),v=b.join(F,"_shared",n),C=b.join(F,i,n);if(!y.existsSync(C))throw new Error(`Starter directory not found: ${i}/${n}`);const $=a?y.mkdtempSync(b.join(Ke.tmpdir(),"create-playcanvas-")):c,I=[M,O,v,...(j.features??[]).flatMap(p=>Mt[p][i].map(k=>b.join(F,"_features",p,k))),C].filter(p=>y.existsSync(p)),R=j.omit??[],D=R.flatMap(p=>[M,O].map(k=>b.join(k,p))),A=Object.keys(De).map(p=>b.join(v,p));for(const p of I){const k=b.join(p,"package.json");y.cpSync(p,$,{recursive:!0,filter:x=>x!==k&&!A.includes(x)&&!D.includes(x)})}for(const[p,k]of Object.entries(De)){const x=b.join($,p);if(!y.existsSync(x))continue;let T=y.readFileSync(x,"utf8");const Q=b.join(v,p);if(y.existsSync(Q)){const J=`${y.readFileSync(Q,"utf8").trimEnd()}
50
+ and upload the zip it writes to dist/ at https://my-lenses.snapchat.com/`},{name:"scene-editor",display:"Scene Editor",category:"Tools",description:"click to select, gizmos and a scene saved in the browser"},{name:"vr-starter",display:"VR Starter",category:"XR",description:"immersive VR, controllers and locomotion"},{name:"ar-placement",display:"AR Placement",category:"XR",description:"mobile AR, hit testing and object placement"}],Nt=async({argFormat:e,yes:t,cancel:r})=>{if(e&&q.some(n=>n.name===e))return e;if(e&&t)throw new Error(`Unknown format: ${e}. Choose from: ${q.map(n=>n.name).join(", ")}`);if(t)return q[0].name;const i=await fe({message:e?`"${e}" isn't a valid format. Please choose from below: `:"Select a format:",options:q.map(n=>({label:n.color(n.display),value:n.name,hint:n.description}))});return Y(i)&&r(),i},Dt=async({argStarter:e,yes:t,cancel:r})=>{if(e&&K.some(n=>n.name===e))return e;if(t){if(!e)return K[0].name;throw new Error(`Unknown starter: ${e}. Choose from: ${K.map(n=>n.name).join(", ")}`)}const i=await fe({message:e?`"${e}" isn't a valid starter. Please choose from below: `:"Select a starter:",initialValue:K[0].name,options:K.map(n=>({label:n.display,value:n.name,hint:`${n.category} \xB7 ${n.description}`}))});return Y(i)&&r(),i},Ne=e=>/^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(e),be=e=>e.trim().toLowerCase().replace(/\s+/g,"-").replace(/^[._]/,"").replace(/[^a-z\d\-~]+/g,"-"),Wt=async({targetDir:e,yes:t,cancel:r})=>{let i=b.basename(b.resolve(e));if(!Ne(i)){if(t)return be(i);const n=await Be({message:"Package name:",defaultValue:be(i),placeholder:be(i),validate:o=>{if(!o||!Ne(o))return"Invalid package.json name"}});if(Y(n))return r();i=n}return i},oe=e=>e.trim().replace(/\/+$/g,""),Vt=e=>{const t=y.readdirSync(e);return t.length===0||t.length===1&&t[0]===".git"},Ut=e=>{if(y.existsSync(e))for(const t of y.readdirSync(e))t!==".git"&&y.rmSync(b.resolve(e,t),{recursive:!0,force:!0})},Gt=async({argTargetDir:e,defaultTargetDir:t,yes:r,cancel:i})=>{let n=e?oe(String(e)):void 0;if(!n&&r)return t;if(!n){const o=await Be({message:"Project name:",defaultValue:t,placeholder:t,validate:l=>{if(l&&!oe(l))return"Invalid project name"}});Y(o)&&i(),n=oe(o)}return n},Kt=async({targetDir:e,argOverwrite:t,argKeep:r,yes:i,cancel:n})=>{if(t&&r)throw new Error("Pass --keep or --overwrite, not both.");if(!y.existsSync(e)||Vt(e))return!1;let o="no";if(t)o="yes";else if(r)o="keep";else{if(i)throw new Error(`Target directory "${e}" is not empty. Pass --keep to add the project to its files, or --overwrite to remove them.`);o=await fe({message:(e==="."?"Current directory":`Target directory "${e}"`)+" is not empty. Please choose how to proceed:",options:[{label:"Cancel operation",value:"no"},{label:"Remove existing files and continue",value:"yes"},{label:"Keep existing files and continue",value:"keep"}]})}if(Y(o))return n();switch(o){case"yes":return Ut(e),!1;case"keep":if(y.existsSync(b.join(e,"package.json")))throw new Error(`Target directory "${e}" already has a package.json, so it can't keep its files. Pass --overwrite to remove them.`);return!0;default:return n()}},Ht=["scripts","dependencies","devDependencies","peerDependencies","engines"],De={"README.md":"## Prerequisites","AGENTS.md":"## Checking your work"},zt="## Agent skills",Jt=["npm","pnpm","yarn","bun"],Yt=e=>e==="yarn"?{install:"yarn",run:"yarn"}:{install:`${e} install`,run:`${e} run`},qt=e=>{const{cwd:t,targetDir:r,format:i,starter:n,packageName:o,skills:l,keep:a,renameFiles:s}=e,c=b.resolve(t,r);y.mkdirSync(c,{recursive:!0});const d=process.env.npm_config_user_agent?.split("/")[0]??"npm",h=Jt.includes(d)?d:"npm",{install:m,run:f}=Yt(h),g=p=>h==="npm"?p:p.replace(/\bnpm run ([^\s`]+) -- /g,`${f} $1 `).replace(/\bnpm run\b/g,f).replace(/\bnpm install\b/g,m);N.step(`Creating project in ${c}...`);const w=He(import.meta.url),_=b.dirname(w),F=e.templatesDir??[b.resolve(_,"../templates"),b.resolve(_,"../../templates")].find(p=>y.existsSync(b.join(p,i)));if(!F)throw new Error(`Format directory not found for: ${i}`);const j=K.find(p=>p.name===n);if(!j)throw new Error(`Starter not found: ${n}`);const M=b.join(F,"_common"),O=b.join(F,i,"base"),v=b.join(F,"_shared",n),C=b.join(F,i,n);if(!y.existsSync(C))throw new Error(`Starter directory not found: ${i}/${n}`);const $=a?y.mkdtempSync(b.join(Ke.tmpdir(),"create-playcanvas-")):c,I=[M,O,v,...(j.features??[]).flatMap(p=>Mt[p][i].map(k=>b.join(F,"_features",p,k))),C].filter(p=>y.existsSync(p)),R=j.omit??[],D=R.flatMap(p=>[M,O].map(k=>b.join(k,p))),A=Object.keys(De).map(p=>b.join(v,p));for(const p of I){const k=b.join(p,"package.json");y.cpSync(p,$,{recursive:!0,filter:x=>x!==k&&!A.includes(x)&&!D.includes(x)})}for(const[p,k]of Object.entries(De)){const x=b.join($,p);if(!y.existsSync(x))continue;let T=y.readFileSync(x,"utf8");const Q=b.join(v,p);if(y.existsSync(Q)){const J=`${y.readFileSync(Q,"utf8").trimEnd()}
51
51
 
52
52
  ${k}`;T=T.replace(k,()=>J)}l||(T=`${T.split(/^(?=## )/m).filter(J=>!J.startsWith(zt)).join("").trimEnd()}
53
53
  `),T=T.split(`
@@ -59,7 +59,7 @@ ${k}`;T=T.replace(k,()=>J)}l||(T=`${T.split(/^(?=## )/m).filter(J=>!J.startsWith
59
59
  ${m}`,z+=`
60
60
  ${f} dev`,j.outro&&(z+=`
61
61
 
62
- ${g(j.outro)}`),Ft(z)},B=Tt(process.argv.slice(2),{alias:{b:"boilerplate",f:"format",h:"help",s:"starter",t:"template",y:"yes"},boolean:["help","overwrite","keep","yes"],string:["format","starter","template","boilerplate"]}),Xt=process.cwd(),We=e=>e.map((t,r)=>` ${t.name.padEnd(27)}${t.description}${r?"":" (default)"}`).join(`
62
+ ${g(j.outro)}`),Ft(z)},P=Tt(process.argv.slice(2),{alias:{b:"boilerplate",f:"format",h:"help",s:"starter",t:"template",y:"yes"},boolean:["help","overwrite","keep","yes"],string:["format","starter","template","boilerplate"]}),Xt=process.cwd(),We=e=>e.map((t,r)=>` ${t.name.padEnd(27)}${t.description}${r?"":" (default)"}`).join(`
63
63
  `),Qt=`Usage: npm create playcanvas@latest [DIRECTORY] -- [OPTION]...
64
64
 
65
65
  Create a new PlayCanvas project with TypeScript in DIRECTORY.
@@ -83,4 +83,4 @@ ${We(K)}
83
83
 
84
84
  Without a terminal, as when an AI agent or CI runs it, the CLI takes the
85
85
  defaults for any option not passed, as --yes does.
86
- `,Zt={_gitignore:".gitignore",_npmrc:".npmrc"},er="playcanvas-project",tr=async()=>{const e=B._[0]?oe(String(B._[0])):void 0,t=B.format??B.template,r=B.starter??B.boilerplate,i=B.overwrite,n=B.keep,o=!!(process.stdin.isTTY&&process.stdout.isTTY),l=B.yes||!o;if(B.help){console.log(Qt);return}!o&&!B.yes&&N.info("Not running in a terminal, so using the defaults for any option not passed");const a=()=>{It("Operation cancelled"),process.exit(0)},s=await Gt({argTargetDir:e,defaultTargetDir:er,yes:l,cancel:a}),c=await Kt({targetDir:s,argOverwrite:i,argKeep:n,yes:l,cancel:a}),d=await Wt({targetDir:s,yes:l,cancel:a}),h=await Nt({argFormat:t,yes:l,cancel:a}),m=await Dt({argStarter:r,yes:l,cancel:a}),f=B.skills??!0;qt({cwd:Xt,targetDir:s,format:h,starter:m,packageName:d,skills:f,keep:c,renameFiles:Zt})};tr().catch(e=>{console.error(e instanceof Error?e.message:e),process.exitCode=1});
86
+ `,Zt={_gitignore:".gitignore",_npmrc:".npmrc"},er="playcanvas-project",tr=async()=>{const e=P._[0]?oe(String(P._[0])):void 0,t=P.format??P.template,r=P.starter??P.boilerplate,i=P.overwrite,n=P.keep,o=!!(process.stdin.isTTY&&process.stdout.isTTY),l=P.yes||!o;if(P.help){console.log(Qt);return}!o&&!P.yes&&N.info("Not running in a terminal, so using the defaults for any option not passed");const a=()=>{It("Operation cancelled"),process.exit(0)},s=await Gt({argTargetDir:e,defaultTargetDir:er,yes:l,cancel:a}),c=await Kt({targetDir:s,argOverwrite:i,argKeep:n,yes:l,cancel:a}),d=await Wt({targetDir:s,yes:l,cancel:a}),h=await Nt({argFormat:t,yes:l,cancel:a}),m=await Dt({argStarter:r,yes:l,cancel:a}),f=P.skills??!0;qt({cwd:Xt,targetDir:s,format:h,starter:m,packageName:d,skills:f,keep:c,renameFiles:Zt})};tr().catch(e=>{console.error(e instanceof Error?e.message:e),process.exitCode=1});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-playcanvas",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Scaffold a PlayCanvas project",
5
5
  "keywords": [
6
6
  "playcanvas",
@@ -0,0 +1,8 @@
1
+ ## Scene Editor
2
+
3
+ This project is a scene editor that saves its scene in the browser.
4
+
5
+ - `src/scene-editor.ts` is the only code that creates, selects, transforms or saves the objects. Add an editing feature there, and let the HUD reach it through the script's `mode`, `add()`, `remove()` and `reset()` and its `editor:select` app event.
6
+ - The scene is saved to `localStorage` after every change and loaded on start, so a saved scene hides a change to `DEFAULT_SCENE`. Press **Reset**, or clear `localStorage`, before you check one.
7
+ - Bump `STORAGE_KEY` whenever the `Item` type changes. A scene saved in the old format would otherwise be loaded as if it were the new one.
8
+ - A click only selects. The script tells a click from a drag by how far the pointer moved, and ignores one that started on a gizmo handle, so keep both checks when you change the pointer handling.
@@ -1,7 +1,20 @@
1
1
  ## Scene Editor
2
2
 
3
- A small editable scene with selectable entities and translate, rotate and scale gizmos.
3
+ A small scene editor. Select an object by clicking it, transform it with a gizmo, add and delete objects, and find the scene as you left it when you come back.
4
4
 
5
5
  ### Controls
6
6
 
7
- Select the cube or sphere, then choose Move, Rotate or Scale to change the active gizmo.
7
+ Click an object to select it, and click empty space to clear the selection. Drag the gizmo to transform the selected object, and choose **Move**, **Rotate** or **Scale** to change the gizmo. Drag anywhere else to orbit the camera. **Add box**, **Add sphere** and **Add cone** add an object at the origin, **Delete** removes the selected one and **Reset** brings back the default scene.
8
+
9
+ ### How it fits together
10
+
11
+ - `src/scene-editor.ts` is the editor, an engine script. It owns the objects, the selection, the gizmo and the saved scene, and it fires an `editor:select` app event for the HUD to follow.
12
+ - `src/main.ts` (`src/App.tsx` in React) sets up the camera, lights and grid, and wires the HUD's buttons to the editor's `mode`, `add()`, `remove()` and `reset()`.
13
+ - The scene is saved to `localStorage` after every change, as a list of `Item`s: a shape with a position, rotation and scale.
14
+
15
+ ### Make it your own
16
+
17
+ - **Edit your own objects.** `create()` builds an entity from an `Item`. Swap the primitive shapes for your own models, and add to `Item` whatever else an object has to remember, such as a color or a name. Bump `STORAGE_KEY` whenever `Item` changes, so a scene saved in the old format is ignored.
18
+ - **Save somewhere else.** `save()` and the load in `initialize()` are the only code that touches `localStorage`. Point them at a file or your own server to share a scene.
19
+ - **Keep the selection and the gizmo.** Neither depends on what the objects are. `pick()` matches the mesh under the pointer to the object that owns it, so change that match if an object becomes a hierarchy of meshes.
20
+ - **Add undo.** There is none yet. Every change already passes through `add()`, `remove()` or the gizmo's `transform:end`, which is where a history would record it.
@@ -1,56 +1,206 @@
1
- import type { Entity, Layer } from 'playcanvas';
2
- import { Gizmo, RotateGizmo, ScaleGizmo, Script, TranslateGizmo } from 'playcanvas';
1
+ // The editor: it owns the objects, the selection, the gizmo and the saved scene, and it is the file
2
+ // to grow to make this starter your own (see "Make it your own" in README.md). The entry file only
3
+ // sets up the camera, light and grid around it, and wires the HUD to `mode`, `add()`, `remove()`,
4
+ // `reset()` and the `editor:select` app event
5
+ import type { Layer, MeshInstance } from 'playcanvas';
6
+ import {
7
+ Color,
8
+ Entity,
9
+ Gizmo,
10
+ Picker,
11
+ RotateGizmo,
12
+ ScaleGizmo,
13
+ Script,
14
+ StandardMaterial,
15
+ TranslateGizmo
16
+ } from 'playcanvas';
3
17
 
4
- type Mode = 'translate' | 'rotate' | 'scale';
18
+ export type Mode = 'translate' | 'rotate' | 'scale';
19
+ export type Shape = 'box' | 'sphere' | 'cone';
20
+
21
+ /** The app event the editor fires whenever the selection changes, for the HUD to follow. */
22
+ export type EditorEvents = {
23
+ 'editor:select': (selected: Entity | null) => void;
24
+ };
25
+
26
+ type Triple = [number, number, number];
27
+
28
+ /** One object, as it is saved. */
29
+ type Item = { shape: Shape; position: Triple; rotation: Triple; scale: Triple };
30
+
31
+ // bump the version whenever Item changes, so a scene saved in the old format is ignored
32
+ const STORAGE_KEY = 'scene-editor:v1';
33
+
34
+ const GIZMOS = { translate: TranslateGizmo, rotate: RotateGizmo, scale: ScaleGizmo };
35
+ const COLORS: Record<Shape, string> = { box: '#ff5145', sphere: '#24addf', cone: '#ffc033' };
36
+ const DEFAULT_SCENE: Item[] = [
37
+ { shape: 'box', position: [-1.2, 0.5, 0], rotation: [0, 0, 0], scale: [1, 1, 1] },
38
+ { shape: 'sphere', position: [1.2, 0.5, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }
39
+ ];
40
+
41
+ // how far the pointer can travel, in CSS pixels, and still count as a click rather than an orbit
42
+ const CLICK_SLOP = 4;
5
43
 
6
44
  class SceneEditor extends Script {
7
45
  static scriptName = 'sceneEditor';
8
46
 
47
+ /** The camera the gizmo and the picker render from. Set it when the script is created. */
9
48
  camera!: Entity;
10
- targets: Entity[] = [];
11
- layer!: Layer;
12
- selected = 0;
49
+
50
+ selected: Entity | null = null;
51
+ items: Entity[] = [];
52
+
13
53
  gizmo?: TranslateGizmo | RotateGizmo | ScaleGizmo;
54
+ gizmoLayer!: Layer;
55
+ gizmoMode: Mode = 'translate';
56
+ picker!: Picker;
57
+ materials = {} as Record<Shape, StandardMaterial>;
58
+
59
+ // where the pointer went down, and whether it went down on one of the gizmo's handles
60
+ downX = 0;
61
+ downY = 0;
62
+ onHandle = false;
63
+
64
+ get mode() {
65
+ return this.gizmoMode;
66
+ }
67
+
68
+ /** Which gizmo the selected object shows. */
69
+ set mode(mode: Mode) {
70
+ if (mode === this.gizmoMode) return;
71
+ this.gizmoMode = mode;
72
+
73
+ // a mode set as the script is created arrives before initialize()
74
+ if (this.gizmo) this.createGizmo();
75
+ }
14
76
 
15
77
  initialize() {
16
- this.layer = Gizmo.createLayer(this.app);
17
- this.setMode('translate');
78
+ this.gizmoLayer = Gizmo.createLayer(this.app);
79
+ this.picker = new Picker(this.app, 1, 1);
80
+ for (const [shape, color] of Object.entries(COLORS) as [Shape, string][]) {
81
+ const material = new StandardMaterial();
82
+ material.diffuse = new Color().fromString(color);
83
+ material.update();
84
+ this.materials[shape] = material;
85
+ }
18
86
 
19
- // wire the scene and gizmo controls
20
- document.querySelectorAll<HTMLButtonElement>('[data-mode]').forEach((button) => {
21
- button.onclick = () => this.setMode(button.dataset.mode as Mode);
22
- });
23
- document.querySelectorAll<HTMLButtonElement>('[data-target]').forEach((button) => {
24
- button.onclick = () => {
25
- this.selected = Number(button.dataset.target);
26
- this.gizmo?.attach([this.targets[this.selected]]);
27
- this.updateButtons();
28
- };
87
+ const saved = localStorage.getItem(STORAGE_KEY);
88
+ this.build(saved ? JSON.parse(saved) : DEFAULT_SCENE);
89
+ this.createGizmo();
90
+
91
+ const { canvas } = this.app.graphicsDevice;
92
+ canvas.addEventListener('pointerdown', this.onPointerDown);
93
+ canvas.addEventListener('pointerup', this.onPointerUp);
94
+
95
+ this.on('destroy', () => {
96
+ canvas.removeEventListener('pointerdown', this.onPointerDown);
97
+ canvas.removeEventListener('pointerup', this.onPointerUp);
98
+ this.build([]);
99
+ this.gizmo?.destroy();
100
+ this.picker.destroy();
101
+ this.app.scene.layers.remove(this.gizmoLayer);
102
+ Object.values(this.materials).forEach((material) => material.destroy());
29
103
  });
30
- this.on('destroy', () => this.gizmo?.destroy());
31
- this.updateButtons();
32
104
  }
33
105
 
34
- // replace the active gizmo for the selected entity
35
- setMode(mode: Mode) {
106
+ /** Adds an object at the origin and selects it, ready to move. */
107
+ add(shape: Shape) {
108
+ this.select(this.create({ shape, position: [0, 0.5, 0], rotation: [0, 0, 0], scale: [1, 1, 1] }));
109
+ this.save();
110
+ }
111
+
112
+ /** Deletes the selected object. */
113
+ remove() {
114
+ if (!this.selected) return;
115
+ this.items.splice(this.items.indexOf(this.selected), 1);
116
+ this.selected.destroy();
117
+ this.select(null);
118
+ this.save();
119
+ }
120
+
121
+ /** Forgets the saved scene and goes back to the default one. */
122
+ reset() {
123
+ localStorage.removeItem(STORAGE_KEY);
124
+ this.build(DEFAULT_SCENE);
125
+ }
126
+
127
+ select(item: Entity | null) {
128
+ this.selected = item;
129
+ if (item) this.gizmo?.attach(item);
130
+ else this.gizmo?.detach();
131
+ this.app.fire('editor:select', item);
132
+ }
133
+
134
+ // replaces every object with the ones in a saved scene
135
+ build(scene: Item[]) {
136
+ this.select(null);
137
+ this.items.splice(0).forEach((item) => item.destroy());
138
+ scene.forEach((item) => this.create(item));
139
+ }
140
+
141
+ create({ shape, position, rotation, scale }: Item) {
142
+ const entity = new Entity(shape);
143
+ entity.addComponent('render', { type: shape, material: this.materials[shape] });
144
+ entity.setLocalPosition(...position);
145
+ entity.setLocalEulerAngles(...rotation);
146
+ entity.setLocalScale(...scale);
147
+ this.entity.addChild(entity);
148
+ this.items.push(entity);
149
+ return entity;
150
+ }
151
+
152
+ save() {
153
+ const scene = this.items.map((item) => ({
154
+ shape: item.render!.type,
155
+ position: item.getLocalPosition().toArray(),
156
+ rotation: item.getLocalEulerAngles().toArray(),
157
+ scale: item.getLocalScale().toArray()
158
+ }));
159
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(scene));
160
+ }
161
+
162
+ // each gizmo is its own class, so changing mode swaps one for another on the same object
163
+ createGizmo() {
36
164
  this.gizmo?.destroy();
37
- const camera = this.camera.camera!;
38
- this.gizmo =
39
- mode === 'rotate'
40
- ? new RotateGizmo(camera, this.layer)
41
- : mode === 'scale'
42
- ? new ScaleGizmo(camera, this.layer)
43
- : new TranslateGizmo(camera, this.layer);
44
- this.gizmo.attach([this.targets[this.selected]]);
45
- document.querySelectorAll<HTMLButtonElement>('[data-mode]').forEach((button) => {
46
- button.ariaPressed = String(button.dataset.mode === mode);
165
+ this.gizmo = new GIZMOS[this.gizmoMode](this.camera.camera!, this.gizmoLayer);
166
+ this.gizmo.on('pointer:down', (_x: number, _y: number, handle?: MeshInstance) => {
167
+ this.onHandle = !!handle;
47
168
  });
48
- }
49
169
 
50
- updateButtons() {
51
- document.querySelectorAll<HTMLButtonElement>('[data-target]').forEach((button) => {
52
- button.ariaPressed = String(Number(button.dataset.target) === this.selected);
170
+ // the camera's own scripts would orbit with the same drag, so hold them until it ends
171
+ this.gizmo.on('transform:start', () => this.holdCamera(true));
172
+ this.gizmo.on('transform:end', () => {
173
+ this.holdCamera(false);
174
+ this.save();
53
175
  });
176
+ if (this.selected) this.gizmo.attach(this.selected);
177
+ }
178
+
179
+ holdCamera(held: boolean) {
180
+ if (this.camera.script) this.camera.script.enabled = !held;
181
+ }
182
+
183
+ onPointerDown = (event: PointerEvent) => {
184
+ this.downX = event.offsetX;
185
+ this.downY = event.offsetY;
186
+ };
187
+
188
+ // a click selects what is under it, or nothing. A drag on a handle or to orbit the camera isn't one
189
+ onPointerUp = (event: PointerEvent) => {
190
+ const moved = Math.hypot(event.offsetX - this.downX, event.offsetY - this.downY);
191
+ if (!this.onHandle && moved <= CLICK_SLOP) this.pick(event.offsetX, event.offsetY);
192
+ this.onHandle = false;
193
+ };
194
+
195
+ // the picker renders the scene's meshes into an offscreen buffer and reads back the one under
196
+ // the pointer, so it is exact for any mesh, and it sees only the World layer, not the gizmo
197
+ async pick(x: number, y: number) {
198
+ const { canvas } = this.app.graphicsDevice;
199
+ const world = this.app.scene.layers.getLayerByName('World')!;
200
+ this.picker.resize(canvas.clientWidth, canvas.clientHeight);
201
+ this.picker.prepare(this.camera.camera!, this.app.scene, [world]);
202
+ const [hit] = await this.picker.getSelectionAsync(x, y);
203
+ this.select(this.items.find((item) => item.render!.meshInstances.includes(hit as MeshInstance)) ?? null);
54
204
  }
55
205
  }
56
206