create-octane-xplat 0.15.0 → 0.16.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.
Files changed (34) hide show
  1. package/README.md +57 -0
  2. package/compatibility.json +199 -0
  3. package/compatibility.ts +109 -0
  4. package/dist/compatibility.js +66 -0
  5. package/dist/index.js +132 -0
  6. package/dist/scaffold.js +439 -0
  7. package/{index.mjs → index.ts} +3 -4
  8. package/library-template/AGENTS.md +3 -1
  9. package/library-template/README.md +20 -0
  10. package/library-template/package.json +22 -19
  11. package/library-template/pnpm-workspace.yaml +150 -11
  12. package/library-template/src/Badge.tsx +1 -1
  13. package/library-template/src/Badge.web.tsx +1 -1
  14. package/library-template/tests/packed-consumer.mjs +2 -1
  15. package/library-template/tsconfig.envs.json +3 -0
  16. package/library-template/tsconfig.json +1 -1
  17. package/library-template/tsrx-typegen.json +1 -1
  18. package/package.json +23 -6
  19. package/{scaffold.mjs → scaffold.ts} +51 -40
  20. package/template/.agents/skills/xplat/SKILL.md +10 -2
  21. package/template/.agents/skills/xplat/references/components.md +2 -2
  22. package/template/.agents/skills/xplat/references/platform.md +2 -2
  23. package/template/AGENTS.md +47 -0
  24. package/template/App_Resources/Android/app.gradle +1 -1
  25. package/template/package.json +33 -31
  26. package/template/pnpm-workspace.yaml +64 -29
  27. package/template/src/app.web.test.tsx +20 -0
  28. package/template/src/main.web.tsrx +1 -0
  29. package/template/src/test-setup.web.ts +17 -0
  30. package/template/tsconfig.json +1 -1
  31. package/template/vite.config.ts +2 -2
  32. package/template/vitest.web.config.mts +13 -0
  33. package/library-template/.oxlintrc.json +0 -24
  34. package/template/.oxlintrc.json +0 -24
package/README.md CHANGED
@@ -16,6 +16,20 @@ With Node.js and pnpm 11.24.0 installed:
16
16
  pnpm create octane-xplat my-app
17
17
  ```
18
18
 
19
+ The starter's `tsconfig.json` selects the shared native JSX types from its
20
+ direct `@octane-xplat/jsx` dependency. Browser JSX leaves select DOM types on
21
+ their first line, as `src/main.web.tsrx` does:
22
+
23
+ ```tsx
24
+ /** @jsxImportSource @octane-xplat/jsx/dom */
25
+ export function BrowserMessage() {
26
+ return <p>Ready</p>
27
+ }
28
+ ```
29
+
30
+ See [platform files and JSX types](https://octane-xplat.goddardai.org/platform/module-resolution#typescript)
31
+ when adding a native or browser implementation.
32
+
19
33
  ## Start a library package
20
34
 
21
35
  To start a reusable package instead of an app, select the library template:
@@ -63,6 +77,49 @@ The generated app pins pnpm 11.24.0. Its `pnpm-workspace.yaml` loads
63
77
  `@octane-xplat/patches` as a `configDependencies` package, making the
64
78
  framework patches available during the first install.
65
79
 
80
+ Apps start with the runtime, build packages, and tests their selected platforms
81
+ need. Feature packages are added when you use them.
82
+
83
+ ## Run app tests
84
+
85
+ New apps include Vitest, configuration, sample tests, and scripts. From your
86
+ app's folder, run the command for a platform you enabled:
87
+
88
+ ```sh
89
+ pnpm test:web # when web is enabled
90
+ pnpm test:ios -- --device DEVICE_UDID # when iOS is enabled
91
+ pnpm test:android -- --device DEVICE_SERIAL # when Android is enabled
92
+ ```
93
+
94
+ Web tests use jsdom, which simulates a browser's
95
+ document on your computer; phone tests use NativeScript's test runner on a
96
+ selected simulator or device. The runners are explicit development dependencies
97
+ with framework-managed versions in `catalog:xplat`. Adapt the sample tests to
98
+ your app as you replace the starter screen.
99
+
100
+ Platforms added later also receive their test setup. The library template
101
+ includes its component tests and publishing checks.
102
+ See [checking an app](../../docs/verify/testing.md#add-automated-tests) for the
103
+ commands, file conventions, and checks that still need a browser or phone.
104
+
105
+ ## Keep compatible versions
106
+
107
+ App and library starters share the framework's compatible compiler, runtime,
108
+ and tool versions through `catalog:xplat`. To update an existing project,
109
+ preview and apply a published CLI release's set, then check the installed
110
+ packages:
111
+
112
+ ```sh
113
+ pnpm dlx @octane-xplat/cli@latest upgrade --check
114
+ pnpm dlx @octane-xplat/cli@latest upgrade
115
+ pnpm xplat doctor
116
+ ```
117
+
118
+ An upgrade covers the workspace's managed dependencies and patches. Your
119
+ app-specific packages and library consumer peer ranges remain yours. See
120
+ [compatible versions](../cli/README.md#compatible-versions) for local links,
121
+ custom patches, deferred installs, and failure recovery.
122
+
66
123
  Leave the server running while you edit. After stopping it, restart from the
67
124
  app directory:
68
125
 
@@ -0,0 +1,199 @@
1
+ {
2
+ "$comment": "Release compatibility set, shared by create-octane-xplat and xplat doctor/upgrade. Edit external pins here, then run pnpm sync:compatibility. Framework package names are generated from the lockstep release set. Optional packages are managed only when declared; library consumer peer ranges remain app-owned.",
3
+ "schemaVersion": 1,
4
+ "frameworkVersion": "0.16.0",
5
+ "tools": {
6
+ "nodeMinimum": "22.18.0",
7
+ "pnpm": "11.24.0"
8
+ },
9
+ "frameworkPackages": [
10
+ "@octane-xplat/audio",
11
+ "@octane-xplat/auth",
12
+ "@octane-xplat/bamboo",
13
+ "@octane-xplat/biometrics",
14
+ "@octane-xplat/camera",
15
+ "@octane-xplat/canvas",
16
+ "@octane-xplat/charts",
17
+ "@octane-xplat/cli",
18
+ "@octane-xplat/context-menu",
19
+ "@octane-xplat/date-picker",
20
+ "@octane-xplat/desktop-webview",
21
+ "@octane-xplat/dnd-kit",
22
+ "@octane-xplat/effects",
23
+ "@octane-xplat/files",
24
+ "@octane-xplat/geolocation",
25
+ "@octane-xplat/gif",
26
+ "@octane-xplat/haptics",
27
+ "@octane-xplat/icons",
28
+ "@octane-xplat/image",
29
+ "@octane-xplat/image-crop",
30
+ "@octane-xplat/intersection-observer",
31
+ "@octane-xplat/jsx",
32
+ "@octane-xplat/lexical",
33
+ "@octane-xplat/lib",
34
+ "@octane-xplat/lingui",
35
+ "@octane-xplat/lint",
36
+ "@octane-xplat/lottie",
37
+ "@octane-xplat/macos-renderer",
38
+ "@octane-xplat/mcp",
39
+ "@octane-xplat/media",
40
+ "@octane-xplat/motion",
41
+ "@octane-xplat/nano-icons",
42
+ "@octane-xplat/notifications",
43
+ "@octane-xplat/pager",
44
+ "@octane-xplat/patches",
45
+ "@octane-xplat/picker",
46
+ "@octane-xplat/platform",
47
+ "@octane-xplat/push",
48
+ "@octane-xplat/recorder",
49
+ "@octane-xplat/richtext",
50
+ "@octane-xplat/rolling-text",
51
+ "@octane-xplat/secure-storage",
52
+ "@octane-xplat/shaders",
53
+ "@octane-xplat/share",
54
+ "@octane-xplat/sheet",
55
+ "@octane-xplat/smooth-corners",
56
+ "@octane-xplat/sounds",
57
+ "@octane-xplat/sqlite",
58
+ "@octane-xplat/table",
59
+ "@octane-xplat/tiptap",
60
+ "@octane-xplat/ui",
61
+ "@octane-xplat/updates",
62
+ "@octane-xplat/video",
63
+ "@octane-xplat/workshop",
64
+ "create-octane-xplat"
65
+ ],
66
+ "packages": {
67
+ "octane": {
68
+ "version": "0.11.0",
69
+ "group": "runtime",
70
+ "singleton": true
71
+ },
72
+ "@nativescript-community/octane": {
73
+ "version": "0.2.5",
74
+ "group": "runtime",
75
+ "singleton": true
76
+ },
77
+ "@nativescript/core": {
78
+ "version": "9.1.3",
79
+ "group": "runtime",
80
+ "singleton": true
81
+ },
82
+ "typescript": {
83
+ "version": "5.9.3",
84
+ "group": "compiler"
85
+ },
86
+ "@tsrx/oxc": {
87
+ "version": "0.20.0",
88
+ "group": "compiler"
89
+ },
90
+ "@tsrx/typescript-plugin": {
91
+ "version": "0.4.11",
92
+ "group": "compiler"
93
+ },
94
+ "tsrx-typegen": {
95
+ "version": "0.2.2",
96
+ "group": "compiler"
97
+ },
98
+ "vite": {
99
+ "version": "8.3.0",
100
+ "group": "bundler"
101
+ },
102
+ "@octanejs/vite-plugin": {
103
+ "version": "0.1.61",
104
+ "group": "bundler"
105
+ },
106
+ "@nativescript-community/vite-octane": {
107
+ "version": "0.2.1",
108
+ "group": "bundler"
109
+ },
110
+ "@nativescript/vite": {
111
+ "version": "8.0.17",
112
+ "group": "bundler"
113
+ },
114
+ "nativescript": {
115
+ "version": "9.1.1",
116
+ "group": "native-toolchain"
117
+ },
118
+ "@nativescript/ios": {
119
+ "version": "9.1.0",
120
+ "group": "native-toolchain"
121
+ },
122
+ "@nativescript/android": {
123
+ "version": "9.1.1",
124
+ "group": "native-toolchain"
125
+ },
126
+ "@nativescript/types": {
127
+ "version": "9.1.4",
128
+ "group": "native-toolchain"
129
+ },
130
+ "@nativescript/types-ios": {
131
+ "version": "9.1.3",
132
+ "group": "native-toolchain"
133
+ },
134
+ "@nativescript/types-android": {
135
+ "version": "9.1.1",
136
+ "group": "native-toolchain"
137
+ },
138
+ "@nativescript/macos-node-api": {
139
+ "version": "0.4.4-next.2026-08-09-31292056208",
140
+ "group": "native-toolchain"
141
+ },
142
+ "@nativescript/windows": {
143
+ "version": "0.1.0-alpha.144",
144
+ "group": "native-toolchain"
145
+ },
146
+ "vitest": {
147
+ "version": "4.1.11",
148
+ "group": "testing"
149
+ },
150
+ "@nativescript/unit-test-runner": {
151
+ "version": "5.0.0",
152
+ "group": "testing"
153
+ },
154
+ "@nativescript-community/gesturehandler": {
155
+ "version": "2.0.45",
156
+ "group": "plugin"
157
+ },
158
+ "@nativescript-community/ui-drawer": {
159
+ "version": "0.1.32",
160
+ "group": "plugin"
161
+ },
162
+ "@nativescript-community/ui-image": {
163
+ "version": "5.0.17",
164
+ "group": "plugin"
165
+ },
166
+ "@valor/nativescript-websockets": {
167
+ "version": "2.0.3",
168
+ "group": "plugin"
169
+ },
170
+ "@triniwiz/nativescript-masonkit": {
171
+ "version": "1.0.0-beta.110",
172
+ "group": "runtime"
173
+ },
174
+ "jsdom": {
175
+ "version": "30.1.1",
176
+ "group": "testing"
177
+ }
178
+ },
179
+ "profiles": {
180
+ "windows": {
181
+ "@nativescript/core": {
182
+ "version": "9.1.3-next.2",
183
+ "specifier": "https://pkg.pr.new/@nativescript/core@7d0adce"
184
+ },
185
+ "@nativescript/vite": {
186
+ "version": "8.0.13",
187
+ "specifier": "https://pkg.pr.new/@nativescript/vite@7d0adce"
188
+ },
189
+ "nativescript": {
190
+ "version": "9.1.2-dev.2026-09-24-36031892256"
191
+ }
192
+ }
193
+ },
194
+ "overrides": {
195
+ "@nativescript/vite>esbuild": "0.27.7",
196
+ "@nativescript-community/vite-octane>esbuild": "0.27.7",
197
+ "vite>esbuild": "0.27.7"
198
+ }
199
+ }
@@ -0,0 +1,109 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { basename, dirname, join, resolve } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { parseDocument } from 'yaml'
6
+
7
+ /** One supported external version, optionally using a preview tarball specifier. */
8
+ export type ManagedPackage = {
9
+ version: string
10
+ specifier?: string
11
+ group: string
12
+ singleton?: boolean
13
+ }
14
+
15
+ /** The release-owned pins used by creator, doctor, and upgrade. */
16
+ export type Compatibility = {
17
+ schemaVersion: number
18
+ frameworkVersion: string
19
+ tools: { nodeMinimum: string; pnpm: string }
20
+ frameworkPackages: string[]
21
+ packages: Record<string, ManagedPackage>
22
+ profiles: Record<string, Record<string, Partial<ManagedPackage>>>
23
+ overrides: Record<string, string>
24
+ }
25
+
26
+ const dir = dirname(fileURLToPath(import.meta.url))
27
+ const root = basename(dir) === 'dist' ? dirname(dir) : dir
28
+ // Keep JSON on disk: Vite's asset URL transform produces a data URL here.
29
+ export const compatibility: Compatibility = createRequire(import.meta.url)(
30
+ join(root, 'compatibility.json'),
31
+ )
32
+
33
+ /** Look up a known framework package or a profile's external dependency pin. */
34
+ export function managedPackage(name: string, profile = 'default'): ManagedPackage | undefined {
35
+ if (compatibility.frameworkPackages.includes(name)) {
36
+ return {
37
+ version: compatibility.frameworkVersion,
38
+ group: 'framework',
39
+ singleton: name === '@octane-xplat/ui',
40
+ }
41
+ }
42
+
43
+ const base = compatibility.packages[name]
44
+ return base ? { ...base, ...compatibility.profiles[profile]?.[name] } : undefined
45
+ }
46
+
47
+ /** Return the pnpm specifier, keeping preview URLs separate from package versions. */
48
+ export function managedSpecifier(name: string, profile = 'default') {
49
+ const entry = managedPackage(name, profile)
50
+ return entry?.specifier ?? entry?.version
51
+ }
52
+
53
+ /** Named pnpm catalog reserved for this compatibility profile. */
54
+ export const managedCatalog = (profile = 'default') =>
55
+ profile === 'default' ? 'xplat' : `xplat-${profile}`
56
+
57
+ /** Installed dependency fields; library consumer peers intentionally keep their ranges. */
58
+ export const dependencySections = [
59
+ 'dependencies',
60
+ 'devDependencies',
61
+ 'optionalDependencies',
62
+ ] as const
63
+
64
+ /** Select Windows preview pins, rejecting a package that also needs mobile pins. */
65
+ export function projectProfile(pkg: Record<string, any>): string {
66
+ const deps = Object.assign({}, ...dependencySections.map((section) => pkg[section]))
67
+ const windows = deps['@nativescript/windows'] || pkg.xplat?.targets?.windows
68
+ if (windows && (deps['@nativescript/ios'] || deps['@nativescript/android'])) {
69
+ throw new Error(
70
+ 'Windows preview and mobile need separate workspace packages: their NativeScript pins differ',
71
+ )
72
+ }
73
+
74
+ return windows ? 'windows' : 'default'
75
+ }
76
+
77
+ /** Add missing pins when enabling a target in an older scaffold. Existing pins stay intact. */
78
+ export function ensureCompatibilityCatalog(appRoot: string, pkg: Record<string, any>) {
79
+ let at = resolve(appRoot)
80
+ while (!existsSync(join(at, 'pnpm-workspace.yaml')) && dirname(at) !== at) {
81
+ at = dirname(at)
82
+ }
83
+ const workspace = existsSync(join(at, 'pnpm-workspace.yaml')) ? at : appRoot
84
+ const path = join(workspace, 'pnpm-workspace.yaml')
85
+ const before = existsSync(path) ? readFileSync(path, 'utf8') : ''
86
+ const doc = parseDocument(before || '{}\n')
87
+ if (doc.errors.length) {
88
+ throw new Error(`${path}: ${doc.errors[0].message}`)
89
+ }
90
+
91
+ const profile = projectProfile(pkg)
92
+ for (const section of dependencySections) {
93
+ for (const [name, spec] of Object.entries(pkg[section] ?? {})) {
94
+ if (spec !== `catalog:${managedCatalog(profile)}`) {
95
+ continue
96
+ }
97
+
98
+ const key = ['catalogs', managedCatalog(profile), name]
99
+ if (!doc.hasIn(key)) {
100
+ doc.setIn(key, managedSpecifier(name, profile))
101
+ }
102
+ }
103
+ }
104
+
105
+ const after = String(doc)
106
+ if (after !== before) {
107
+ writeFileSync(path, after)
108
+ }
109
+ }
@@ -0,0 +1,66 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseDocument } from "yaml";
5
+ import { createRequire } from "node:module";
6
+ //#region compatibility.ts
7
+ var dir = dirname(fileURLToPath(import.meta.url));
8
+ var root = basename(dir) === "dist" ? dirname(dir) : dir;
9
+ var compatibility = createRequire(import.meta.url)(join(root, "compatibility.json"));
10
+ /** Look up a known framework package or a profile's external dependency pin. */
11
+ function managedPackage(name, profile = "default") {
12
+ if (compatibility.frameworkPackages.includes(name)) return {
13
+ version: compatibility.frameworkVersion,
14
+ group: "framework",
15
+ singleton: name === "@octane-xplat/ui"
16
+ };
17
+ const base = compatibility.packages[name];
18
+ return base ? {
19
+ ...base,
20
+ ...compatibility.profiles[profile]?.[name]
21
+ } : void 0;
22
+ }
23
+ /** Return the pnpm specifier, keeping preview URLs separate from package versions. */
24
+ function managedSpecifier(name, profile = "default") {
25
+ const entry = managedPackage(name, profile);
26
+ return entry?.specifier ?? entry?.version;
27
+ }
28
+ /** Named pnpm catalog reserved for this compatibility profile. */
29
+ var managedCatalog = (profile = "default") => profile === "default" ? "xplat" : `xplat-${profile}`;
30
+ /** Installed dependency fields; library consumer peers intentionally keep their ranges. */
31
+ var dependencySections = [
32
+ "dependencies",
33
+ "devDependencies",
34
+ "optionalDependencies"
35
+ ];
36
+ /** Select Windows preview pins, rejecting a package that also needs mobile pins. */
37
+ function projectProfile(pkg) {
38
+ const deps = Object.assign({}, ...dependencySections.map((section) => pkg[section]));
39
+ const windows = deps["@nativescript/windows"] || pkg.xplat?.targets?.windows;
40
+ if (windows && (deps["@nativescript/ios"] || deps["@nativescript/android"])) throw new Error("Windows preview and mobile need separate workspace packages: their NativeScript pins differ");
41
+ return windows ? "windows" : "default";
42
+ }
43
+ /** Add missing pins when enabling a target in an older scaffold. Existing pins stay intact. */
44
+ function ensureCompatibilityCatalog(appRoot, pkg) {
45
+ let at = resolve(appRoot);
46
+ while (!existsSync(join(at, "pnpm-workspace.yaml")) && dirname(at) !== at) at = dirname(at);
47
+ const workspace = existsSync(join(at, "pnpm-workspace.yaml")) ? at : appRoot;
48
+ const path = join(workspace, "pnpm-workspace.yaml");
49
+ const before = existsSync(path) ? readFileSync(path, "utf8") : "";
50
+ const doc = parseDocument(before || "{}\n");
51
+ if (doc.errors.length) throw new Error(`${path}: ${doc.errors[0].message}`);
52
+ const profile = projectProfile(pkg);
53
+ for (const section of dependencySections) for (const [name, spec] of Object.entries(pkg[section] ?? {})) {
54
+ if (spec !== `catalog:${managedCatalog(profile)}`) continue;
55
+ const key = [
56
+ "catalogs",
57
+ managedCatalog(profile),
58
+ name
59
+ ];
60
+ if (!doc.hasIn(key)) doc.setIn(key, managedSpecifier(name, profile));
61
+ }
62
+ const after = String(doc);
63
+ if (after !== before) writeFileSync(path, after);
64
+ }
65
+ //#endregion
66
+ export { compatibility, dependencySections, ensureCompatibilityCatalog, managedCatalog, managedPackage, managedSpecifier, projectProfile };
package/dist/index.js ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ import { composeLibrary, composeTargets, defaultTargets, resolveTargets, selectableTargets, targets } from "./scaffold.js";
3
+ import { existsSync, readdirSync } from "node:fs";
4
+ import { basename, resolve } from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ //#region index.ts
7
+ var argv = process.argv.slice(2);
8
+ var noInstall = argv.includes("--no-install");
9
+ var templateIndex = argv.findIndex((arg) => arg === "--template" || arg.startsWith("--template="));
10
+ var templateValueIndex = templateIndex !== -1 && argv[templateIndex] === "--template" ? templateIndex + 1 : -1;
11
+ var templateArg = templateIndex === -1 ? "app" : templateValueIndex !== -1 ? argv[templateValueIndex] : argv[templateIndex].slice(11);
12
+ var nameIndex = argv.findIndex((arg) => arg === "--name" || arg.startsWith("--name="));
13
+ var nameValueIndex = nameIndex !== -1 && argv[nameIndex] === "--name" ? nameIndex + 1 : -1;
14
+ var nameArg = nameIndex === -1 ? void 0 : nameValueIndex !== -1 ? argv[nameValueIndex] : argv[nameIndex].slice(7);
15
+ if (templateIndex !== -1 && (!templateArg || templateArg.startsWith("-"))) {
16
+ console.error("✗ --template needs a value: app or library");
17
+ process.exit(1);
18
+ }
19
+ if (!["app", "library"].includes(templateArg)) {
20
+ console.error(`✗ unknown template "${templateArg}" — expected app or library`);
21
+ process.exit(1);
22
+ }
23
+ if (nameIndex !== -1 && (!nameArg || nameArg.startsWith("-"))) {
24
+ console.error("✗ --name needs an npm package name");
25
+ process.exit(1);
26
+ }
27
+ var targetsIndex = argv.findIndex((arg) => arg === "--targets" || arg.startsWith("--targets="));
28
+ var targetsValueIndex = targetsIndex !== -1 && argv[targetsIndex] === "--targets" ? targetsIndex + 1 : -1;
29
+ var targetsArg = targetsIndex === -1 ? void 0 : targetsValueIndex !== -1 ? argv[targetsValueIndex] : argv[targetsIndex].slice(10);
30
+ if (targetsIndex !== -1 && (targetsArg === void 0 || targetsArg.startsWith("-"))) {
31
+ console.error(`✗ --targets needs a comma list: ${selectableTargets.join(", ")}`);
32
+ process.exit(1);
33
+ }
34
+ var optionValues = new Set([
35
+ targetsValueIndex,
36
+ templateValueIndex,
37
+ nameValueIndex
38
+ ].filter((index) => index !== -1));
39
+ var positional = argv.filter((arg, i) => !arg.startsWith("-") && !optionValues.has(i));
40
+ var defaultDir = templateArg === "library" ? "octane-xplat-library" : "octane-xplat-app";
41
+ var dir = resolve(positional[0] ?? defaultDir);
42
+ if (templateArg === "library" && targetsIndex !== -1) {
43
+ console.error("✗ --targets applies to the app template; libraries start with web and native builds");
44
+ process.exit(1);
45
+ }
46
+ if (templateArg === "app" && nameIndex !== -1) {
47
+ console.error("✗ --name applies only to the library template");
48
+ process.exit(1);
49
+ }
50
+ if (existsSync(dir) && readdirSync(dir).length > 0) {
51
+ console.error(`✗ ${dir} exists and is not empty`);
52
+ process.exit(1);
53
+ }
54
+ var parseTargets = (value) => {
55
+ const ids = value.split(",").map((s) => s.trim()).filter(Boolean);
56
+ if (ids.length === 0) throw new Error(`--targets needs a comma list: ${selectableTargets.join(", ")}`);
57
+ for (const id of ids) if (!selectableTargets.includes(id)) throw new Error(`unknown target "${id}" — expected a comma list of: ${selectableTargets.join(", ")}`);
58
+ return resolveTargets(ids);
59
+ };
60
+ var ids = [];
61
+ if (templateArg === "library") {
62
+ const packageName = nameArg ?? basename(dir);
63
+ try {
64
+ composeLibrary(dir, packageName);
65
+ } catch (error) {
66
+ console.error(`✗ ${error.message}`);
67
+ process.exit(1);
68
+ }
69
+ console.log(`\n✓ scaffolded ${dir} (library: ${packageName}; web + iOS + Android)`);
70
+ } else {
71
+ if (targetsArg !== void 0) try {
72
+ ids = parseTargets(targetsArg);
73
+ } catch (error) {
74
+ console.error(`✗ ${error.message}`);
75
+ process.exit(1);
76
+ }
77
+ else if (process.stdout.isTTY) {
78
+ const p = await import("@clack/prompts");
79
+ p.intro("create-octane-xplat");
80
+ const picked = await p.multiselect({
81
+ message: "Target platforms",
82
+ options: selectableTargets.map((id) => ({
83
+ value: id,
84
+ label: targets[id].label,
85
+ hint: targets[id].note
86
+ })),
87
+ initialValues: [...defaultTargets],
88
+ required: true
89
+ });
90
+ if (p.isCancel(picked)) {
91
+ p.cancel("Cancelled");
92
+ process.exit(0);
93
+ }
94
+ ids = picked;
95
+ } else ids = defaultTargets;
96
+ composeTargets(ids, dir);
97
+ console.log(`\n✓ scaffolded ${dir} (${[...new Set(resolveTargets(ids))].filter((id) => !targets[id].hidden).join(", ")})`);
98
+ }
99
+ if (noInstall) {
100
+ console.log("\nSkipped install (--no-install). To run manually:");
101
+ printNextSteps(dir, ids, templateArg);
102
+ process.exit(0);
103
+ }
104
+ var install = spawnSync("pnpm", ["install"], {
105
+ cwd: dir,
106
+ stdio: "inherit"
107
+ });
108
+ if (install.status !== 0) {
109
+ console.log(`\nInstall didn't finish (pnpm missing?). To run manually:`);
110
+ printNextSteps(dir, ids, templateArg);
111
+ process.exit(install.status ?? 1);
112
+ }
113
+ if (templateArg === "app" && ids.includes("web")) {
114
+ console.log("\n✓ installed — starting dev server (Ctrl+C to stop)\n");
115
+ spawnSync("pnpm", ["dev"], {
116
+ cwd: dir,
117
+ stdio: "inherit"
118
+ });
119
+ } else {
120
+ console.log("\n✓ installed. Next steps:");
121
+ printNextSteps(dir, ids, templateArg);
122
+ }
123
+ function printNextSteps(dir, ids, template) {
124
+ console.log(` cd ${dir}\n pnpm install`);
125
+ if (template === "library") {
126
+ console.log(" pnpm typecheck\n pnpm lint\n pnpm typegen\n pnpm test\n pnpm test:packed");
127
+ return;
128
+ }
129
+ if (ids.includes("web")) console.log(" pnpm dev");
130
+ else console.log(" pnpm xplat dev");
131
+ }
132
+ //#endregion