vite-plus 0.2.2 → 0.2.4

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.
@@ -1,13 +1,13 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "../rolldown-runtime-C7HZzL1F.js";
2
- import { s as VITE_PLUS_NAME } from "../constants-NeTOxrzV.js";
3
- import { A as intro, B as runCommandSilently, C as resolveApproveBuildTargets, E as PackageManager, F as spinner, I as text, L as require_picocolors, M as multiselect, O as cancel, P as select, R as isCancel, S as detectGatedBuilds, T as DependencyType, V as require_cross_spawn, d as downloadPackageManager$1, f as promptGitHooks, g as selectPackageManager, h as runViteInstall, j as log, k as confirm, m as runViteFmt, p as resolveGitInit, u as defaultInteractive, x as approveBuilds, z as runCommand$1 } from "../tsconfig-fvpxgUq2.js";
2
+ import { s as VITE_PLUS_NAME } from "../constants-DdXYYBsz.js";
3
+ import { A as intro, B as runCommandSilently, C as resolveApproveBuildTargets, E as PackageManager, F as spinner, I as text, L as require_picocolors, M as multiselect, O as cancel, P as select, R as isCancel, S as detectGatedBuilds, T as DependencyType, V as require_cross_spawn, d as downloadPackageManager$1, f as promptGitHooks, g as selectPackageManager, h as runViteInstall, j as log, k as confirm, m as runViteFmt, p as resolveGitInit, u as defaultInteractive, x as approveBuilds, z as runCommand$1 } from "../tsconfig-CtdiYBV_.js";
4
4
  import { a as printHeader, i as muted, o as success, r as log$1, t as accent } from "../terminal-uTv0ZaMr.js";
5
5
  import { i as readJsonFile, n as editJsonFile } from "../json-DiRs8ceZ.js";
6
- import { a as resolveViteConfig, n as findWorkspaceRoot, r as hasViteConfig, t as findViteConfig } from "../resolve-vite-config-r91rIaPs.js";
6
+ import { a as resolveViteConfig, n as findWorkspaceRoot, r as hasViteConfig, t as findViteConfig } from "../resolve-vite-config-5SuNAKdv.js";
7
7
  import { t as lib_default } from "../lib-L3DWSRQp.js";
8
- import { o as fetchNpmResource, s as getNpmRegistry, t as checkNpmPackageExists } from "../package-CU2g7URl.js";
9
- import { A as detectWorkspace$1, F as detectEslintProject, M as updatePackageJsonWithDeps, N as updateWorkspaceConfig, R as promptEslintMigration, S as injectCreateDefaultTemplate, X as templatesDir, Y as displayRelative, a as selectAgentTargets, c as writeCopilotSetupWorkflow, d as rewriteStandaloneProject, g as setPackageManager, j as isBingoTemplate, l as rewriteMonorepo, r as detectExistingAgentTargetPaths, s as writeAgentInstructions, t as COPILOT_AGENT_ID, u as rewriteMonorepoProject, y as installGitHooks } from "../agent-D7O7mSeO.js";
10
- import { a as writeEditorConfigs, c as hasFrameworkShim, f as promptPrettierMigration, i as selectEditors, n as detectExistingEditors, o as addFrameworkShim, s as detectFramework, u as detectPrettierProject } from "../editor-CPzssglc.js";
8
+ import { o as fetchNpmResource, s as getNpmRegistry, t as checkNpmPackageExists } from "../package-D2xMMmTQ.js";
9
+ import { A as detectWorkspace$1, F as detectEslintProject, M as updatePackageJsonWithDeps, N as updateWorkspaceConfig, R as promptEslintMigration, S as injectCreateDefaultTemplate, X as templatesDir, Y as displayRelative, a as selectAgentTargets, c as writeCopilotSetupWorkflow, d as rewriteStandaloneProject, g as setPackageManager, j as isBingoTemplate, l as rewriteMonorepo, r as detectExistingAgentTargetPaths, s as writeAgentInstructions, t as COPILOT_AGENT_ID, u as rewriteMonorepoProject, y as installGitHooks } from "../agent-eMtwgY-u.js";
10
+ import { a as writeEditorConfigs, c as hasFrameworkShim, f as promptPrettierMigration, i as selectEditors, n as detectExistingEditors, o as addFrameworkShim, s as detectFramework, u as detectPrettierProject } from "../editor-DL4u-ujK.js";
11
11
  import { t as renderCliDoc } from "../help-YP84FSEz.js";
12
12
  import path from "node:path";
13
13
  import { runCommand, upsertJsonConfig, vitePlusHeader } from "../../binding/index.js";
@@ -105,829 +105,861 @@ function prependToPathToEnvs(extraPath, envs) {
105
105
  return envs;
106
106
  }
107
107
  //#endregion
108
- //#region src/create/org-manifest.ts
109
- /**
110
- * Parse the org picker specifier: `@scope` (scope only → picker) or
111
- * `@scope:name` (direct manifest-entry selection). Colon mirrors the
112
- * existing `vite:monorepo` / `vite:library` builtin-template syntax and
113
- * keeps manifest entries syntactically distinct from real
114
- * `@scope/package-name` npm specifiers.
115
- *
116
- * Returns `null` for anything else — including the plain `@scope/name`
117
- * form, which routes to the existing `@scope/create-name` shorthand as
118
- * it did before the org-manifest feature.
119
- *
120
- * The optional `version` suffix (`@scope@1.2.3`, `@scope:name@1.2.3`)
121
- * pins `@scope/create` to a specific release rather than `dist-tags.latest`.
122
- */
123
- function parseOrgScopedSpec(spec) {
124
- if (!spec.startsWith("@")) return null;
125
- if (spec.includes("/")) return null;
126
- const colonIndex = spec.indexOf(":");
127
- if (colonIndex === -1) {
128
- const atIndex = spec.indexOf("@", 1);
129
- if (atIndex === -1) return { scope: spec };
130
- const version = spec.slice(atIndex + 1);
131
- return version ? {
132
- scope: spec.slice(0, atIndex),
133
- version
134
- } : { scope: spec.slice(0, atIndex) };
135
- }
136
- const scope = spec.slice(0, colonIndex);
137
- const rest = spec.slice(colonIndex + 1);
138
- const atIndex = rest.indexOf("@");
139
- const name = atIndex === -1 ? rest : rest.slice(0, atIndex);
140
- const version = atIndex === -1 ? "" : rest.slice(atIndex + 1);
141
- if (!name) return version ? {
142
- scope,
143
- version
144
- } : { scope };
145
- return version ? {
146
- scope,
147
- name,
148
- version
149
- } : {
150
- scope,
151
- name
152
- };
153
- }
154
- /**
155
- * Schema-level failure. Never falls through silently — a maintainer who
156
- * shipped an invalid manifest should see the offending field.
157
- */
158
- var OrgManifestSchemaError = class extends Error {
159
- packageName;
160
- constructor(message, packageName) {
161
- super(`${packageName}: ${message}`);
162
- this.packageName = packageName;
163
- this.name = "OrgManifestSchemaError";
164
- }
108
+ //#region ../../node_modules/.pnpm/nanotar@0.3.0/node_modules/nanotar/dist/index.mjs
109
+ const tarItemTypeMap = {
110
+ "0": "file",
111
+ "1": "hardLink",
112
+ "2": "symbolicLink",
113
+ "3": "characterDevice",
114
+ "4": "blockDevice",
115
+ "5": "directory",
116
+ "6": "fifo",
117
+ "7": "contiguousFile",
118
+ "g": "globalExtendedHeader",
119
+ "x": "extendedHeader",
120
+ "D": "gnuDirectory",
121
+ "I": "gnuInodeMetadata",
122
+ "K": "gnuLongLinkName",
123
+ "L": "gnuLongFileName",
124
+ "N": "gnuOldLongFileName",
125
+ "M": "gnuMultiVolume",
126
+ "S": "gnuSparseFile",
127
+ "E": "gnuExtendedSparse",
128
+ "A": "solarisAcl",
129
+ "V": "solarisVolumeLabel",
130
+ "X": "solarisOldExtendedHeader"
165
131
  };
166
- function isRelativePath(spec) {
167
- return spec.startsWith("./") || spec.startsWith("../");
168
- }
169
- /**
170
- * Validate the `{ name, description, template }` fields shared by org manifest
171
- * entries and local `create.templates` entries. `label` is the config path
172
- * used in error messages (e.g. `createConfig.templates` or `create.templates`)
173
- * and `makeError` builds the thrown error so each source uses its own type.
174
- */
175
- function validateTemplateEntry(entry, index, label, makeError) {
176
- if (!entry || typeof entry !== "object") throw makeError(`${label}[${index}] must be an object`);
177
- const raw = entry;
178
- const requireString = (field) => {
179
- const value = raw[field];
180
- if (typeof value !== "string" || value.length === 0) throw makeError(`${label}[${index}].${field} must be a non-empty string`);
181
- return value;
182
- };
183
- const name = requireString("name");
184
- if (name.startsWith("__vp_")) throw makeError(`${label}[${index}].name uses the reserved \`__vp_\` prefix`);
185
- const description = requireString("description");
186
- const template = requireString("template");
187
- if (isRelativePath(template)) {
188
- const resolved = path.posix.resolve("/root", template.replaceAll("\\", "/"));
189
- if (resolved !== "/root" && !resolved.startsWith("/root/")) throw makeError(`${label}[${index}].template escapes the package root: ${template}`);
132
+ function parseTar(data, opts) {
133
+ const buffer = data.buffer || data;
134
+ const files = [];
135
+ let offset = 0;
136
+ let nextExtendedHeader;
137
+ let globalExtendedHeader;
138
+ while (offset < buffer.byteLength - 512) {
139
+ let name = _readString(buffer, offset, 100);
140
+ if (name.length === 0) break;
141
+ if (nextExtendedHeader) {
142
+ const longName = nextExtendedHeader.path || nextExtendedHeader.linkpath;
143
+ if (longName) name = longName;
144
+ }
145
+ const mode = _readString(buffer, offset + 100, 8).trim();
146
+ const uid = Number.parseInt(_readString(buffer, offset + 108, 8));
147
+ const gid = Number.parseInt(_readString(buffer, offset + 116, 8));
148
+ const size = _readNumber(buffer, offset + 124, 12);
149
+ const seek = 512 + 512 * Math.trunc(size / 512) + (size % 512 ? 512 : 0);
150
+ const mtime = _readNumber(buffer, offset + 136, 12);
151
+ const _type = _readString(buffer, offset + 156, 1) || "0";
152
+ const type = tarItemTypeMap[_type] || _type;
153
+ switch (type) {
154
+ case "extendedHeader":
155
+ case "globalExtendedHeader": {
156
+ const headers = _parseExtendedHeaders(new Uint8Array(buffer, offset + 512, size));
157
+ if (type === "extendedHeader") nextExtendedHeader = headers;
158
+ else {
159
+ nextExtendedHeader = void 0;
160
+ globalExtendedHeader = {
161
+ ...globalExtendedHeader,
162
+ ...headers
163
+ };
164
+ }
165
+ offset += seek;
166
+ continue;
167
+ }
168
+ case "gnuLongFileName":
169
+ case "gnuOldLongFileName":
170
+ case "gnuLongLinkName":
171
+ nextExtendedHeader = { path: _readString(buffer, offset + 512, size) };
172
+ offset += seek;
173
+ continue;
174
+ }
175
+ const user = _readString(buffer, offset + 265, 32);
176
+ const group = _readString(buffer, offset + 297, 32);
177
+ name = _sanitizePath(name);
178
+ const meta = {
179
+ name,
180
+ type,
181
+ size,
182
+ attrs: {
183
+ ...globalExtendedHeader,
184
+ ...nextExtendedHeader,
185
+ mode,
186
+ uid,
187
+ gid,
188
+ mtime,
189
+ user,
190
+ group
191
+ }
192
+ };
193
+ nextExtendedHeader = void 0;
194
+ if (opts?.filter && !opts.filter(meta)) {
195
+ offset += seek;
196
+ continue;
197
+ }
198
+ if (opts?.metaOnly) {
199
+ files.push(meta);
200
+ offset += seek;
201
+ continue;
202
+ }
203
+ const data2 = size === 0 ? void 0 : new Uint8Array(buffer, offset + 512, size);
204
+ files.push({
205
+ ...meta,
206
+ data: data2,
207
+ get text() {
208
+ return new TextDecoder().decode(this.data);
209
+ }
210
+ });
211
+ offset += seek;
190
212
  }
191
- return {
192
- name,
193
- description,
194
- template
195
- };
213
+ return files;
196
214
  }
197
- /**
198
- * Validate a list of entries, rejecting duplicate `name`s. Shared by org
199
- * manifests and local `create.templates`.
200
- */
201
- function validateTemplateEntries(templates, label, makeError, validateOne) {
202
- const entries = [];
203
- const seen = /* @__PURE__ */ new Set();
204
- for (let index = 0; index < templates.length; index += 1) {
205
- const entry = validateOne(templates[index], index);
206
- if (seen.has(entry.name)) throw makeError(`${label}[${index}].name duplicates an earlier entry: "${entry.name}"`);
207
- seen.add(entry.name);
208
- entries.push(entry);
209
- }
210
- return entries;
215
+ async function parseTarGzip(data, opts = {}) {
216
+ const stream = new ReadableStream({ start(controller) {
217
+ controller.enqueue(new Uint8Array(data));
218
+ controller.close();
219
+ } }).pipeThrough(new DecompressionStream(opts.compression ?? "gzip"));
220
+ return parseTar(await new Response(stream).arrayBuffer(), opts);
211
221
  }
212
- function validateEntry(entry, index, packageName) {
213
- const makeError = (message) => new OrgManifestSchemaError(message, packageName);
214
- const base = validateTemplateEntry(entry, index, "createConfig.templates", makeError);
215
- let monorepo;
216
- const raw = entry;
217
- if (raw.monorepo !== void 0) {
218
- if (typeof raw.monorepo !== "boolean") throw makeError(`createConfig.templates[${index}].monorepo must be a boolean`);
219
- monorepo = raw.monorepo;
220
- }
221
- return {
222
- ...base,
223
- ...monorepo !== void 0 ? { monorepo } : {}
224
- };
222
+ function _sanitizePath(path) {
223
+ let normalized = path.replace(/\\/g, "/");
224
+ normalized = normalized.replace(/^[a-zA-Z]:\//, "");
225
+ normalized = normalized.replace(/^\/+/, "");
226
+ const hasLeadingDotSlash = normalized.startsWith("./");
227
+ const parts = normalized.split("/");
228
+ const resolved = [];
229
+ for (const part of parts) if (part === "..") resolved.pop();
230
+ else if (part !== "." && part !== "") resolved.push(part);
231
+ let result = resolved.join("/");
232
+ if (hasLeadingDotSlash && !result.startsWith("./")) result = "./" + result;
233
+ if (path.endsWith("/") && !result.endsWith("/")) result += "/";
234
+ return result;
225
235
  }
226
- function validateManifest(raw, packageName) {
227
- if (!raw || typeof raw !== "object") return null;
228
- const createConfig = raw.createConfig;
229
- if (!createConfig || typeof createConfig !== "object") return null;
230
- const templates = createConfig.templates;
231
- if (templates === void 0) return null;
232
- if (!Array.isArray(templates)) throw new OrgManifestSchemaError("createConfig.templates must be an array", packageName);
233
- if (templates.length === 0) return null;
234
- return validateTemplateEntries(templates, "createConfig.templates", (message) => new OrgManifestSchemaError(message, packageName), (entry, index) => validateEntry(entry, index, packageName));
236
+ function _readString(buffer, offset, size) {
237
+ const view = new Uint8Array(buffer, offset, size);
238
+ const i = view.indexOf(0);
239
+ return new TextDecoder().decode(i === -1 ? view : view.slice(0, i));
235
240
  }
236
- /**
237
- * Schema-level failure for `create.templates` in `vite.config.ts`. A misconfigured
238
- * local template should surface clearly rather than silently disappear.
239
- */
240
- var CreateConfigSchemaError = class extends Error {
241
- constructor(message) {
242
- super(message);
243
- this.name = "CreateConfigSchemaError";
241
+ function _readNumber(buffer, offset, size) {
242
+ const view = new Uint8Array(buffer, offset, size);
243
+ let str = "";
244
+ for (let i = 0; i < size; i++) str += String.fromCodePoint(view[i]);
245
+ return Number.parseInt(str, 8);
246
+ }
247
+ function _parseExtendedHeaders(data) {
248
+ const dataStr = new TextDecoder().decode(data);
249
+ const headers = {};
250
+ for (const line of dataStr.split("\n")) {
251
+ const s = line.split(" ")[1]?.split("=");
252
+ if (s) headers[s[0]] = s[1];
244
253
  }
245
- };
246
- /**
247
- * Validate `create.templates` from `vite.config.ts`. Returns `[]` when the field
248
- * is absent or an empty array; throws {@link CreateConfigSchemaError} when present
249
- * but malformed.
250
- */
251
- function validateCreateTemplates(templates) {
252
- if (templates === void 0) return [];
253
- if (!Array.isArray(templates)) throw new CreateConfigSchemaError("create.templates must be an array");
254
- const makeError = (message) => new CreateConfigSchemaError(message);
255
- return validateTemplateEntries(templates, "create.templates", makeError, (entry, index) => {
256
- const validated = validateTemplateEntry(entry, index, "create.templates", makeError);
257
- if (validated.name.startsWith("vite:")) throw makeError(`create.templates[${index}].name uses the reserved \`vite:\` prefix`);
258
- return validated;
259
- });
254
+ return headers;
260
255
  }
261
- async function fetchPackument(scope, packageName) {
262
- const response = await fetchNpmResource(`${getNpmRegistry(scope)}/${packageName}`, {
263
- headers: { accept: "application/json" },
264
- timeoutMs: 5e3
265
- });
266
- if (response.status === 404) return null;
267
- if (!response.ok) throw new Error(`npm registry responded with ${response.status} for ${packageName}`);
268
- return await response.json();
256
+ //#endregion
257
+ //#region src/create/org-tarball.ts
258
+ function getCacheRoot() {
259
+ const home = process.env.VP_HOME || path.join(os.homedir(), ".vite-plus");
260
+ return path.join(home, "tmp", "create-org");
269
261
  }
270
262
  /**
271
- * Fetch `@scope/create` from the npm registry and parse its `createConfig.templates`
272
- * manifest.
273
- *
274
- * Returns `null` when:
275
- * - the package does not exist on the registry (404), or
276
- * - the package exists but has no `createConfig.templates` field
277
- *
278
- * Throws when:
279
- * - the `createConfig.templates` field is present but malformed (`OrgManifestSchemaError`), or
280
- * - the registry request fails for any non-404 reason
281
- *
282
- * `requestedVersion` pins the lookup to a specific `versions[...]` entry
283
- * (equivalent to `vp create @scope@1.2.3`); omit it to resolve `dist-tags.latest`.
263
+ * Replace characters that are illegal in Windows path segments
264
+ * (`\ / : * ? " < > |` plus the IPv6 bracket pair `[ ]`). The host
265
+ * comes from `new URL(...).host` which can carry a port (`:4873`) or
266
+ * IPv6 literal (`[::1]`); both end up in the cache path otherwise.
284
267
  */
285
- async function readOrgManifest(scope, requestedVersion) {
286
- if (!scope.startsWith("@")) return null;
287
- const packageName = `${scope}/create`;
288
- const packument = await fetchPackument(scope, packageName);
289
- if (!packument) return null;
290
- let resolvedVersion;
291
- if (requestedVersion) {
292
- resolvedVersion = packument["dist-tags"]?.[requestedVersion] ?? (packument.versions?.[requestedVersion] ? requestedVersion : void 0);
293
- if (!resolvedVersion) throw new OrgManifestSchemaError(`version "${requestedVersion}" not found (known tags: ${Object.keys(packument["dist-tags"] ?? {}).join(", ") || "none"})`, packageName);
294
- } else {
295
- resolvedVersion = packument["dist-tags"]?.latest;
296
- if (!resolvedVersion) return null;
297
- }
298
- const meta = packument.versions?.[resolvedVersion];
299
- if (!meta) return null;
300
- const templates = validateManifest(meta, packageName);
301
- if (!templates) return null;
302
- if (!meta.dist?.tarball) throw new OrgManifestSchemaError(`missing dist.tarball for ${resolvedVersion}`, packageName);
303
- return {
304
- scope,
305
- packageName,
306
- version: resolvedVersion,
307
- tarballUrl: meta.dist.tarball,
308
- integrity: meta.dist.integrity,
309
- templates
310
- };
268
+ function sanitizeHostForPath(host) {
269
+ return host.replaceAll(/[\\/:*?"<>|[\]]/g, "_");
311
270
  }
312
271
  /**
313
- * Apply the in-monorepo filter rule from the RFC: entries with
314
- * `monorepo: true` are hidden when the command is invoked inside an
315
- * existing monorepo, mirroring `initial-template-options.ts:9-31`.
272
+ * Cache extracted tarballs under `<host>/<scope>/create/<version>` so two
273
+ * repos resolving the same `<scope>@<version>` through different registries
274
+ * (via `.npmrc` scope mappings) don't share a cache slot. The registry
275
+ * guarantees `manifest.tarballUrl` is a valid URL, so any parse failure
276
+ * here is a real bug worth surfacing.
316
277
  */
317
- function filterManifestForContext(templates, isMonorepo) {
318
- if (!isMonorepo) return [...templates];
319
- return templates.filter((entry) => !entry.monorepo);
320
- }
321
- //#endregion
322
- //#region src/create/templates/types.ts
323
- const LibraryTemplateRepo = "github:sxzz/tsdown-templates/vite-plus";
324
- const BuiltinTemplate = {
325
- generator: "vite:generator",
326
- monorepo: "vite:monorepo",
327
- application: "vite:application",
328
- library: "vite:library"
329
- };
330
- const TemplateType = {
331
- builtin: "builtin",
332
- bingo: "bingo",
333
- remote: "remote",
334
- bundled: "bundled"
335
- };
336
- //#endregion
337
- //#region src/create/discovery.ts
338
- function isGitHubUrl(templateName) {
339
- return templateName.startsWith("https://github.com/") || templateName.startsWith("github:") || templateName.includes("github.com/");
340
- }
341
- function parseGitHubUrl(url) {
342
- if (url.startsWith("github:")) return url.slice(7);
343
- const match = url.match(/github\.com\/([^/]+\/[^/]+)/);
344
- if (match) return match[1].replace(/\.git$/, "");
345
- return null;
346
- }
347
- function inferGitHubRepoName(templateName) {
348
- const degitPath = parseGitHubUrl(templateName);
349
- if (!degitPath) return null;
350
- return degitPath.split("/").pop() || null;
278
+ function getExtractionDir(manifest) {
279
+ const { host } = new URL(manifest.tarballUrl);
280
+ return path.join(getCacheRoot(), sanitizeHostForPath(host), manifest.scope, "create", manifest.version);
351
281
  }
352
- function localTemplateDir(workspaceInfo, templateName) {
353
- if (isRelativePath(templateName)) return templateName.replace(/^\.\//, "");
354
- return workspaceInfo.packages.find((pkg) => pkg.name === templateName)?.path;
282
+ function parseIntegrity(integrity) {
283
+ const match = integrity.split(/\s+/)[0].match(/^(sha\d+)-(.+)$/);
284
+ if (!match) return null;
285
+ return {
286
+ algorithm: match[1],
287
+ expected: match[2]
288
+ };
355
289
  }
356
- function resolveLocalBinPath(localPackagePath, packageName, bin) {
357
- if (!bin) return;
358
- if (typeof bin === "string") return path.join(localPackagePath, bin);
359
- const entries = Object.entries(bin);
360
- if (entries.length === 0) return;
361
- if (entries.length === 1) return path.join(localPackagePath, entries[0][1]);
362
- const unscopedName = packageName.slice(packageName.lastIndexOf("/") + 1);
363
- const preferred = bin[packageName] ?? bin[unscopedName];
364
- if (preferred) return path.join(localPackagePath, preferred);
365
- throw new Error(`Local template package "${packageName}" defines multiple "bin" entries (${entries.map(([name]) => name).join(", ")}); add a "bin" entry named "${packageName}" so the template entry is unambiguous`);
290
+ function verifyIntegrity(bytes, integrity) {
291
+ if (!integrity) return;
292
+ const parsed = parseIntegrity(integrity);
293
+ if (!parsed) return;
294
+ const hash = createHash(parsed.algorithm);
295
+ hash.update(bytes);
296
+ const actual = hash.digest("base64");
297
+ if (actual !== parsed.expected) throw new Error(`integrity check failed: expected ${integrity}, got ${parsed.algorithm}-${actual}`);
366
298
  }
367
- function discoverTemplate(templateName, templateArgs, workspaceInfo, interactive, bundledLocalPath, skipShorthand, localTemplate) {
368
- const envs = prependToPathToEnvs(workspaceInfo.downloadPackageManager.binPrefix, { ...process.env });
369
- const parentDir = inferParentDir(templateName, workspaceInfo, localTemplate);
370
- if (bundledLocalPath) return {
371
- command: "",
372
- args: [...templateArgs],
373
- envs,
374
- type: TemplateType.bundled,
375
- parentDir,
376
- interactive,
377
- localPath: bundledLocalPath
378
- };
379
- if (templateName.startsWith("vite:")) return {
380
- command: templateName,
381
- args: [...templateArgs],
382
- envs,
383
- type: TemplateType.builtin,
384
- parentDir,
385
- interactive
386
- };
387
- if (isGitHubUrl(templateName)) {
388
- const degitPath = parseGitHubUrl(templateName);
389
- if (degitPath) return {
390
- command: "degit",
391
- args: [degitPath, ...templateArgs],
392
- envs,
393
- type: TemplateType.remote,
394
- parentDir,
395
- interactive
396
- };
397
- }
398
- if (localTemplate) {
399
- const localDir = localTemplateDir(workspaceInfo, templateName);
400
- if (!localDir) throw new Error(`Local template "${templateName}" does not match any workspace package; update the \`create.templates\` entry in vite.config.ts`);
401
- const localPackagePath = path.join(workspaceInfo.rootDir, localDir);
402
- const packageJsonPath = path.join(localPackagePath, "package.json");
403
- if (!fs.existsSync(packageJsonPath)) throw new Error(`Local template "${templateName}" has no package.json, so it cannot be run as a template`);
404
- const pkg = readJsonFile(packageJsonPath);
405
- const binPath = resolveLocalBinPath(localPackagePath, pkg.name ?? templateName, pkg.bin);
406
- if (!binPath) throw new Error(`Local template "${templateName}" has no "bin" entry in its package.json, so it cannot be run as a template`);
407
- const args = [binPath, ...templateArgs];
408
- let type = TemplateType.remote;
409
- if (isBingoTemplate(pkg)) {
410
- type = TemplateType.bingo;
411
- args.push("--skip-requests");
299
+ const MAX_TARBALL_BYTES = 50 * 1024 * 1024;
300
+ async function downloadTarball(url) {
301
+ const response = await fetchNpmResource(url, { timeoutMs: 3e4 });
302
+ if (!response.ok) throw new Error(`failed to download tarball (${response.status}): ${url}`);
303
+ const contentLength = Number(response.headers.get("content-length"));
304
+ if (Number.isFinite(contentLength) && contentLength > MAX_TARBALL_BYTES) throw new Error(`tarball exceeds ${MAX_TARBALL_BYTES} byte size limit: ${url}`);
305
+ const reader = response.body?.getReader();
306
+ if (!reader) throw new Error(`tarball response has no body: ${url}`);
307
+ const chunks = [];
308
+ let total = 0;
309
+ while (true) {
310
+ const { done, value } = await reader.read();
311
+ if (done) break;
312
+ total += value.byteLength;
313
+ if (total > MAX_TARBALL_BYTES) {
314
+ await reader.cancel();
315
+ throw new Error(`tarball exceeds ${MAX_TARBALL_BYTES} byte size limit: ${url}`);
412
316
  }
413
- return {
414
- command: "node",
415
- args,
416
- envs,
417
- type,
418
- parentDir,
419
- interactive
420
- };
317
+ chunks.push(value);
421
318
  }
422
- return {
423
- command: skipShorthand ? templateName : expandCreateShorthand(templateName),
424
- args: [...templateArgs],
425
- envs,
426
- type: TemplateType.remote,
427
- parentDir,
428
- interactive
429
- };
319
+ const bytes = new Uint8Array(total);
320
+ let offset = 0;
321
+ for (const chunk of chunks) {
322
+ bytes.set(chunk, offset);
323
+ offset += chunk.byteLength;
324
+ }
325
+ return bytes;
430
326
  }
431
327
  /**
432
- * Expand shorthand template names to their full `create-*` package names.
433
- *
434
- * This follows the same convention as `npm create` / `pnpm create`:
435
- * - `vite` → `create-vite`
436
- * - `vite@latest` → `create-vite@latest`
328
+ * Download a package tarball and parse its `package/package.json`.
437
329
  *
438
- * Special cases for packages where the convention doesn't work:
439
- * - `nitro` `create-nitro-app` (create-nitro is abandoned)
440
- * - `svelte` → `sv`
441
- * - `@tanstack/start` `@tanstack/cli` (@tanstack/create-start is deprecated)
330
+ * Some registries (GitHub Packages among them) strip fields they don't
331
+ * recognize including `createConfig` from packument *version metadata*
332
+ * while preserving the published tarball byte-for-byte. This gives
333
+ * `readOrgManifest` a fallback source of truth for those registries.
442
334
  *
443
- * Skips expansion for:
444
- * - Builtin templates (`vite:*`)
445
- * - GitHub URLs
446
- * - Local paths (`./`, `../`, `/`)
447
- * - Names already starting with `create-` (or `@scope/create-`)
335
+ * Returns `null` when the archive contains no `package/package.json`.
336
+ * Throws on download/integrity failures or unparsable JSON.
448
337
  */
449
- function expandCreateShorthand(templateName) {
450
- if (templateName.includes(":")) return templateName;
451
- if (isGitHubUrl(templateName)) return templateName;
452
- if (templateName.startsWith("./") || templateName.startsWith("../") || templateName.startsWith("/")) return templateName;
453
- if (templateName.startsWith("@")) {
454
- const slashIndex = templateName.indexOf("/");
455
- if (slashIndex === -1) {
456
- const atIndex = templateName.indexOf("@", 1);
457
- return `${atIndex === -1 ? templateName : templateName.slice(0, atIndex)}/create${atIndex === -1 ? "" : templateName.slice(atIndex)}`;
338
+ async function readPackageJsonFromTarball(tarballUrl, integrity) {
339
+ const bytes = await downloadTarball(tarballUrl);
340
+ verifyIntegrity(bytes, integrity);
341
+ const entries = await parseTarGzip(bytes);
342
+ for (const entry of entries) {
343
+ if (normalizeEntryName(entry.name) !== "package.json" || !entry.data) continue;
344
+ const text = new TextDecoder().decode(entry.data);
345
+ try {
346
+ return JSON.parse(text);
347
+ } catch {
348
+ throw new Error(`invalid package.json in tarball: ${tarballUrl}`);
458
349
  }
459
- const scope = templateName.slice(0, slashIndex);
460
- const rest = templateName.slice(slashIndex + 1);
461
- const atIndex = rest.indexOf("@");
462
- const name = atIndex === -1 ? rest : rest.slice(0, atIndex);
463
- const version = atIndex === -1 ? "" : rest.slice(atIndex);
464
- if (name.startsWith("create-")) return templateName;
465
- if (scope === "@tanstack" && name === "start") return `@tanstack/cli${version}`;
466
- return `${scope}/create-${name}${version}`;
467
350
  }
468
- const atIndex = templateName.indexOf("@");
469
- const name = atIndex === -1 ? templateName : templateName.slice(0, atIndex);
470
- const version = atIndex === -1 ? "" : templateName.slice(atIndex);
471
- if (name.startsWith("create-")) return templateName;
472
- if (name === "nitro") return `create-nitro-app${version}`;
473
- if (name === "svelte") return `sv${version}`;
474
- return `create-${name}${version}`;
351
+ return null;
475
352
  }
476
- function inferParentDir(templateName, workspaceInfo, localTemplate = false) {
477
- if (workspaceInfo.parentDirs.length === 0) return;
478
- const localDir = localTemplate ? localTemplateDir(workspaceInfo, templateName) : void 0;
479
- if (localDir) {
480
- const ownParentDir = path.dirname(localDir);
481
- if (workspaceInfo.parentDirs.includes(ownParentDir)) return ownParentDir;
353
+ const STAGING_SUFFIX_PREFIX = ".tmp-";
354
+ /**
355
+ * Parse a tar entry's stored mode (always octal) into the numeric
356
+ * permission bits (low 9 bits — `rwxrwxrwx`). Returns `undefined` when
357
+ * the mode is missing or unparsable so the caller leaves the file with
358
+ * its default (umask-derived) permissions instead of downgrading.
359
+ */
360
+ function parseEntryMode(raw) {
361
+ if (!raw) return;
362
+ const parsed = Number.parseInt(raw, 8);
363
+ if (!Number.isFinite(parsed)) return;
364
+ return parsed & 511;
365
+ }
366
+ /**
367
+ * Strip the `package/` prefix from an `npm pack` tarball entry. Returns
368
+ * `null` for entries to skip (root dir, PaxHeader, anything outside
369
+ * `package/`).
370
+ */
371
+ function normalizeEntryName(rawName) {
372
+ const name = rawName.replace(/^\.\//, "").replace(/\\/g, "/");
373
+ if (!name || name === "package" || name === "package/") return null;
374
+ if (name.startsWith("PaxHeader/") || name.includes("/PaxHeader/")) return null;
375
+ if (!name.startsWith("package/")) return null;
376
+ return name.slice(8);
377
+ }
378
+ async function extractTarballTo(bytes, destDir) {
379
+ const entries = await parseTarGzip(bytes);
380
+ const stagingDir = `${destDir}${STAGING_SUFFIX_PREFIX}${process.pid}-${Date.now()}`;
381
+ await fs.promises.mkdir(stagingDir, { recursive: true });
382
+ const resolvedStaging = path.resolve(stagingDir);
383
+ try {
384
+ for (const entry of entries) {
385
+ const relativeName = normalizeEntryName(entry.name);
386
+ if (relativeName === null) continue;
387
+ const targetPath = path.join(stagingDir, relativeName);
388
+ const resolvedTarget = path.resolve(targetPath);
389
+ if (resolvedTarget !== resolvedStaging && !resolvedTarget.startsWith(`${resolvedStaging}${path.sep}`)) throw new Error(`tarball entry escapes extraction root: ${entry.name}`);
390
+ if (entry.type === "directory" || relativeName.endsWith("/")) {
391
+ await fs.promises.mkdir(targetPath, { recursive: true });
392
+ continue;
393
+ }
394
+ await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
395
+ const data = entry.data ?? /* @__PURE__ */ new Uint8Array(0);
396
+ await fs.promises.writeFile(targetPath, data);
397
+ const mode = parseEntryMode(entry.attrs?.mode);
398
+ if (mode !== void 0) await fs.promises.chmod(targetPath, mode);
399
+ }
400
+ try {
401
+ await fs.promises.rename(stagingDir, destDir);
402
+ } catch (error) {
403
+ const code = error.code;
404
+ if ((code === "ENOTEMPTY" || code === "EEXIST") && fs.existsSync(path.join(destDir, "package.json"))) {
405
+ await fs.promises.rm(stagingDir, {
406
+ recursive: true,
407
+ force: true
408
+ }).catch(() => {});
409
+ return;
410
+ }
411
+ throw error;
412
+ }
413
+ } catch (error) {
414
+ await fs.promises.rm(stagingDir, {
415
+ recursive: true,
416
+ force: true
417
+ }).catch(() => {});
418
+ throw error;
482
419
  }
483
- let rule = /app/i;
484
- if (templateName === BuiltinTemplate.library) rule = /lib|component|package/i;
485
- else if (templateName === BuiltinTemplate.generator) rule = /generator|tool/i;
486
- for (const parentDir of workspaceInfo.parentDirs) if (rule.test(parentDir)) return parentDir;
420
+ }
421
+ const STAGING_STALE_MS = 1440 * 60 * 1e3;
422
+ /**
423
+ * Remove `<destDir>.tmp-*` siblings left behind by a previous crash so
424
+ * repeated aborts don't accumulate orphaned staging trees. Only deletes
425
+ * entries whose mtime is older than 24 hours — a concurrent `vp create`
426
+ * that's still actively extracting will always be younger than that, so
427
+ * the age gate keeps this safe to run at the top of every extract.
428
+ */
429
+ async function cleanupStaleStagingDirs(destDir) {
430
+ const parent = path.dirname(destDir);
431
+ const prefix = `${path.basename(destDir)}${STAGING_SUFFIX_PREFIX}`;
432
+ let entries;
433
+ try {
434
+ entries = await fs.promises.readdir(parent);
435
+ } catch {
436
+ return;
437
+ }
438
+ const cutoff = Date.now() - STAGING_STALE_MS;
439
+ await Promise.all(entries.filter((name) => name.startsWith(prefix)).map(async (name) => {
440
+ const fullPath = path.join(parent, name);
441
+ try {
442
+ if ((await fs.promises.stat(fullPath)).mtimeMs < cutoff) await fs.promises.rm(fullPath, {
443
+ recursive: true,
444
+ force: true
445
+ });
446
+ } catch {}
447
+ }));
448
+ }
449
+ /**
450
+ * Ensure the `@org/create` package tarball for the given manifest has been
451
+ * downloaded and extracted locally. Returns the absolute path to the
452
+ * extracted package root (i.e. the directory that contains
453
+ * `package.json`).
454
+ *
455
+ * Idempotent: subsequent calls for the same `<scope, version>` reuse the
456
+ * cached extraction. Concurrent calls race on the final rename; the loser
457
+ * cleans up and returns the existing directory.
458
+ */
459
+ async function ensureOrgPackageExtracted(manifest) {
460
+ const extractedRoot = getExtractionDir(manifest);
461
+ if (fs.existsSync(path.join(extractedRoot, "package.json"))) return extractedRoot;
462
+ const parent = path.dirname(extractedRoot);
463
+ await fs.promises.mkdir(parent, { recursive: true });
464
+ await cleanupStaleStagingDirs(extractedRoot);
465
+ const bytes = await downloadTarball(manifest.tarballUrl);
466
+ verifyIntegrity(bytes, manifest.integrity);
467
+ await extractTarballTo(bytes, extractedRoot);
468
+ return extractedRoot;
469
+ }
470
+ /**
471
+ * Resolve a manifest entry's relative `./...` path against an already-
472
+ * extracted package root, rejecting any path that escapes the root (via
473
+ * `..` walks or an absolute specifier).
474
+ *
475
+ * Existence is NOT checked here — the subsequent `copyDir` surfaces any
476
+ * missing-directory error with a clearer errno.
477
+ */
478
+ function resolveBundledPath(extractedRoot, relativePath) {
479
+ if (path.isAbsolute(relativePath)) throw new Error(`bundled template path must be relative, got ${relativePath}`);
480
+ const resolvedRoot = path.resolve(extractedRoot);
481
+ const resolvedTarget = path.resolve(extractedRoot, relativePath);
482
+ if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`)) throw new Error(`bundled template path escapes the package root: ${relativePath}`);
483
+ return resolvedTarget;
487
484
  }
488
485
  //#endregion
489
- //#region src/create/initial-template-options.ts
490
- function getInitialTemplateOptions(isMonorepo, templates = []) {
491
- return [
492
- ...!isMonorepo ? [{
493
- label: "Vite+ Monorepo",
494
- value: BuiltinTemplate.monorepo,
495
- hint: "Create a new Vite+ monorepo project"
496
- }] : [],
497
- {
498
- label: "Vite+ Application",
499
- value: BuiltinTemplate.application,
500
- hint: "Create vite applications"
501
- },
502
- {
503
- label: "Vite+ Library",
504
- value: BuiltinTemplate.library,
505
- hint: "Create vite libraries"
506
- },
507
- ...isMonorepo ? templates.map((entry) => ({
508
- label: entry.name,
509
- value: entry.name,
510
- hint: entry.description
511
- })) : []
512
- ];
486
+ //#region src/create/org-manifest.ts
487
+ /**
488
+ * Parse the org picker specifier: `@scope` (scope only → picker) or
489
+ * `@scope:name` (direct manifest-entry selection). Colon mirrors the
490
+ * existing `vite:monorepo` / `vite:library` builtin-template syntax and
491
+ * keeps manifest entries syntactically distinct from real
492
+ * `@scope/package-name` npm specifiers.
493
+ *
494
+ * Returns `null` for anything else — including the plain `@scope/name`
495
+ * form, which routes to the existing `@scope/create-name` shorthand as
496
+ * it did before the org-manifest feature.
497
+ *
498
+ * The optional `version` suffix (`@scope@1.2.3`, `@scope:name@1.2.3`)
499
+ * pins `@scope/create` to a specific release rather than `dist-tags.latest`.
500
+ */
501
+ function parseOrgScopedSpec(spec) {
502
+ if (!spec.startsWith("@")) return null;
503
+ if (spec.includes("/")) return null;
504
+ const colonIndex = spec.indexOf(":");
505
+ if (colonIndex === -1) {
506
+ const atIndex = spec.indexOf("@", 1);
507
+ if (atIndex === -1) return { scope: spec };
508
+ const version = spec.slice(atIndex + 1);
509
+ return version ? {
510
+ scope: spec.slice(0, atIndex),
511
+ version
512
+ } : { scope: spec.slice(0, atIndex) };
513
+ }
514
+ const scope = spec.slice(0, colonIndex);
515
+ const rest = spec.slice(colonIndex + 1);
516
+ const atIndex = rest.indexOf("@");
517
+ const name = atIndex === -1 ? rest : rest.slice(0, atIndex);
518
+ const version = atIndex === -1 ? "" : rest.slice(atIndex + 1);
519
+ if (!name) return version ? {
520
+ scope,
521
+ version
522
+ } : { scope };
523
+ return version ? {
524
+ scope,
525
+ name,
526
+ version
527
+ } : {
528
+ scope,
529
+ name
530
+ };
513
531
  }
514
- //#endregion
515
- //#region src/create/org-picker.ts
516
- const ORG_PICKER_CANCEL = Symbol("org-picker-cancel");
517
- const ORG_PICKER_BUILTIN_ESCAPE = Symbol("org-picker-builtin-escape");
518
- const ESCAPE_HATCH = Symbol("builtin-escape");
519
532
  /**
520
- * Render the interactive picker for an org manifest. Always appends a
521
- * trailing "Vite+ built-in templates" escape-hatch entry.
522
- *
523
- * Context-filters entries with `monorepo: true` when running inside an
524
- * existing monorepo, mirroring `initial-template-options.ts:9-31`.
525
- *
526
- * Returns `ORG_PICKER_BUILTIN_ESCAPE` when the escape hatch is selected,
527
- * or `ORG_PICKER_CANCEL` when the user hits Ctrl-C.
533
+ * Schema-level failure. Never falls through silently a maintainer who
534
+ * shipped an invalid manifest should see the offending field.
528
535
  */
529
- async function pickOrgTemplate(manifest, opts) {
530
- const filtered = filterManifestForContext(manifest.templates, opts.isMonorepo);
531
- if (filtered.length === 0) return ORG_PICKER_BUILTIN_ESCAPE;
532
- const escapeValue = `__vp_builtin_escape__::${randomUUID()}`;
533
- const lookup = /* @__PURE__ */ new Map();
534
- const options = filtered.map((entry) => {
535
- lookup.set(entry.name, entry);
536
- return {
537
- value: entry.name,
538
- label: entry.name,
539
- hint: entry.description
540
- };
541
- });
542
- lookup.set(escapeValue, ESCAPE_HATCH);
543
- const builtinHint = opts.isMonorepo ? "Use defaults (application / library)" : "Use defaults (monorepo / application / library)";
544
- options.push({
545
- value: escapeValue,
546
- label: "Vite+ built-in templates",
547
- hint: builtinHint
548
- });
549
- const picked = await select({
550
- message: `Pick a template from ${manifest.scope}`,
551
- options
552
- });
553
- if (isCancel(picked)) return ORG_PICKER_CANCEL;
554
- const found = lookup.get(picked);
555
- if (found === ESCAPE_HATCH) return ORG_PICKER_BUILTIN_ESCAPE;
556
- if (!found) throw new Error(`org-picker: prompts.select returned an unregistered value: ${picked}`);
557
- return {
558
- kind: "entry",
559
- entry: found
560
- };
536
+ var OrgManifestSchemaError = class extends Error {
537
+ packageName;
538
+ constructor(message, packageName) {
539
+ super(`${packageName}: ${message}`);
540
+ this.packageName = packageName;
541
+ this.name = "OrgManifestSchemaError";
542
+ }
543
+ };
544
+ function isRelativePath(spec) {
545
+ return spec.startsWith("./") || spec.startsWith("../");
561
546
  }
562
547
  /**
563
- * Render the manifest as a plain-text table for the `--no-interactive`
564
- * error output. Fixed column order so AI agents and scripts can recover
565
- * available template names without a `--json` flag.
548
+ * Validate the `{ name, description, template }` fields shared by org manifest
549
+ * entries and local `create.templates` entries. `label` is the config path
550
+ * used in error messages (e.g. `createConfig.templates` or `create.templates`)
551
+ * and `makeError` builds the thrown error so each source uses its own type.
566
552
  */
567
- function formatManifestTable(manifest, isMonorepo) {
568
- const visible = filterManifestForContext(manifest.templates, isMonorepo);
569
- const filteredCount = manifest.templates.length - visible.length;
570
- const nameWidth = Math.max(4, ...visible.map((entry) => entry.name.length));
571
- const descWidth = Math.max(11, ...visible.map((entry) => entry.description.length));
572
- const lines = [];
573
- lines.push(` ${"NAME".padEnd(nameWidth)} ${"DESCRIPTION".padEnd(descWidth)} TEMPLATE`);
574
- for (const entry of visible) lines.push(` ${entry.name.padEnd(nameWidth)} ${entry.description.padEnd(descWidth)} ${entry.template}`);
553
+ function validateTemplateEntry(entry, index, label, makeError) {
554
+ if (!entry || typeof entry !== "object") throw makeError(`${label}[${index}] must be an object`);
555
+ const raw = entry;
556
+ const requireString = (field) => {
557
+ const value = raw[field];
558
+ if (typeof value !== "string" || value.length === 0) throw makeError(`${label}[${index}].${field} must be a non-empty string`);
559
+ return value;
560
+ };
561
+ const name = requireString("name");
562
+ if (name.startsWith("__vp_")) throw makeError(`${label}[${index}].name uses the reserved \`__vp_\` prefix`);
563
+ const description = requireString("description");
564
+ const template = requireString("template");
565
+ if (isRelativePath(template)) {
566
+ const resolved = path.posix.resolve("/root", template.replaceAll("\\", "/"));
567
+ if (resolved !== "/root" && !resolved.startsWith("/root/")) throw makeError(`${label}[${index}].template escapes the package root: ${template}`);
568
+ }
575
569
  return {
576
- lines,
577
- filteredCount
570
+ name,
571
+ description,
572
+ template
578
573
  };
579
574
  }
580
- //#endregion
581
- //#region ../../node_modules/.pnpm/nanotar@0.3.0/node_modules/nanotar/dist/index.mjs
582
- const tarItemTypeMap = {
583
- "0": "file",
584
- "1": "hardLink",
585
- "2": "symbolicLink",
586
- "3": "characterDevice",
587
- "4": "blockDevice",
588
- "5": "directory",
589
- "6": "fifo",
590
- "7": "contiguousFile",
591
- "g": "globalExtendedHeader",
592
- "x": "extendedHeader",
593
- "D": "gnuDirectory",
594
- "I": "gnuInodeMetadata",
595
- "K": "gnuLongLinkName",
596
- "L": "gnuLongFileName",
597
- "N": "gnuOldLongFileName",
598
- "M": "gnuMultiVolume",
599
- "S": "gnuSparseFile",
600
- "E": "gnuExtendedSparse",
601
- "A": "solarisAcl",
602
- "V": "solarisVolumeLabel",
603
- "X": "solarisOldExtendedHeader"
604
- };
605
- function parseTar(data, opts) {
606
- const buffer = data.buffer || data;
607
- const files = [];
608
- let offset = 0;
609
- let nextExtendedHeader;
610
- let globalExtendedHeader;
611
- while (offset < buffer.byteLength - 512) {
612
- let name = _readString(buffer, offset, 100);
613
- if (name.length === 0) break;
614
- if (nextExtendedHeader) {
615
- const longName = nextExtendedHeader.path || nextExtendedHeader.linkpath;
616
- if (longName) name = longName;
617
- }
618
- const mode = _readString(buffer, offset + 100, 8).trim();
619
- const uid = Number.parseInt(_readString(buffer, offset + 108, 8));
620
- const gid = Number.parseInt(_readString(buffer, offset + 116, 8));
621
- const size = _readNumber(buffer, offset + 124, 12);
622
- const seek = 512 + 512 * Math.trunc(size / 512) + (size % 512 ? 512 : 0);
623
- const mtime = _readNumber(buffer, offset + 136, 12);
624
- const _type = _readString(buffer, offset + 156, 1) || "0";
625
- const type = tarItemTypeMap[_type] || _type;
626
- switch (type) {
627
- case "extendedHeader":
628
- case "globalExtendedHeader": {
629
- const headers = _parseExtendedHeaders(new Uint8Array(buffer, offset + 512, size));
630
- if (type === "extendedHeader") nextExtendedHeader = headers;
631
- else {
632
- nextExtendedHeader = void 0;
633
- globalExtendedHeader = {
634
- ...globalExtendedHeader,
635
- ...headers
636
- };
637
- }
638
- offset += seek;
639
- continue;
640
- }
641
- case "gnuLongFileName":
642
- case "gnuOldLongFileName":
643
- case "gnuLongLinkName":
644
- nextExtendedHeader = { path: _readString(buffer, offset + 512, size) };
645
- offset += seek;
646
- continue;
647
- }
648
- const user = _readString(buffer, offset + 265, 32);
649
- const group = _readString(buffer, offset + 297, 32);
650
- name = _sanitizePath(name);
651
- const meta = {
652
- name,
653
- type,
654
- size,
655
- attrs: {
656
- ...globalExtendedHeader,
657
- ...nextExtendedHeader,
658
- mode,
659
- uid,
660
- gid,
661
- mtime,
662
- user,
663
- group
664
- }
665
- };
666
- nextExtendedHeader = void 0;
667
- if (opts?.filter && !opts.filter(meta)) {
668
- offset += seek;
669
- continue;
670
- }
671
- if (opts?.metaOnly) {
672
- files.push(meta);
673
- offset += seek;
674
- continue;
675
- }
676
- const data2 = size === 0 ? void 0 : new Uint8Array(buffer, offset + 512, size);
677
- files.push({
678
- ...meta,
679
- data: data2,
680
- get text() {
681
- return new TextDecoder().decode(this.data);
682
- }
683
- });
684
- offset += seek;
575
+ /**
576
+ * Validate a list of entries, rejecting duplicate `name`s. Shared by org
577
+ * manifests and local `create.templates`.
578
+ */
579
+ function validateTemplateEntries(templates, label, makeError, validateOne) {
580
+ const entries = [];
581
+ const seen = /* @__PURE__ */ new Set();
582
+ for (let index = 0; index < templates.length; index += 1) {
583
+ const entry = validateOne(templates[index], index);
584
+ if (seen.has(entry.name)) throw makeError(`${label}[${index}].name duplicates an earlier entry: "${entry.name}"`);
585
+ seen.add(entry.name);
586
+ entries.push(entry);
685
587
  }
686
- return files;
687
- }
688
- async function parseTarGzip(data, opts = {}) {
689
- const stream = new ReadableStream({ start(controller) {
690
- controller.enqueue(new Uint8Array(data));
691
- controller.close();
692
- } }).pipeThrough(new DecompressionStream(opts.compression ?? "gzip"));
693
- return parseTar(await new Response(stream).arrayBuffer(), opts);
694
- }
695
- function _sanitizePath(path) {
696
- let normalized = path.replace(/\\/g, "/");
697
- normalized = normalized.replace(/^[a-zA-Z]:\//, "");
698
- normalized = normalized.replace(/^\/+/, "");
699
- const hasLeadingDotSlash = normalized.startsWith("./");
700
- const parts = normalized.split("/");
701
- const resolved = [];
702
- for (const part of parts) if (part === "..") resolved.pop();
703
- else if (part !== "." && part !== "") resolved.push(part);
704
- let result = resolved.join("/");
705
- if (hasLeadingDotSlash && !result.startsWith("./")) result = "./" + result;
706
- if (path.endsWith("/") && !result.endsWith("/")) result += "/";
707
- return result;
708
- }
709
- function _readString(buffer, offset, size) {
710
- const view = new Uint8Array(buffer, offset, size);
711
- const i = view.indexOf(0);
712
- return new TextDecoder().decode(i === -1 ? view : view.slice(0, i));
713
- }
714
- function _readNumber(buffer, offset, size) {
715
- const view = new Uint8Array(buffer, offset, size);
716
- let str = "";
717
- for (let i = 0; i < size; i++) str += String.fromCodePoint(view[i]);
718
- return Number.parseInt(str, 8);
588
+ return entries;
719
589
  }
720
- function _parseExtendedHeaders(data) {
721
- const dataStr = new TextDecoder().decode(data);
722
- const headers = {};
723
- for (const line of dataStr.split("\n")) {
724
- const s = line.split(" ")[1]?.split("=");
725
- if (s) headers[s[0]] = s[1];
590
+ function validateEntry(entry, index, packageName) {
591
+ const makeError = (message) => new OrgManifestSchemaError(message, packageName);
592
+ const base = validateTemplateEntry(entry, index, "createConfig.templates", makeError);
593
+ let monorepo;
594
+ const raw = entry;
595
+ if (raw.monorepo !== void 0) {
596
+ if (typeof raw.monorepo !== "boolean") throw makeError(`createConfig.templates[${index}].monorepo must be a boolean`);
597
+ monorepo = raw.monorepo;
726
598
  }
727
- return headers;
599
+ return {
600
+ ...base,
601
+ ...monorepo !== void 0 ? { monorepo } : {}
602
+ };
728
603
  }
729
- //#endregion
730
- //#region src/create/org-tarball.ts
731
- function getCacheRoot() {
732
- const home = process.env.VP_HOME || path.join(os.homedir(), ".vite-plus");
733
- return path.join(home, "tmp", "create-org");
604
+ function validateManifest(raw, packageName) {
605
+ if (!raw || typeof raw !== "object") return null;
606
+ const createConfig = raw.createConfig;
607
+ if (!createConfig || typeof createConfig !== "object") return null;
608
+ const templates = createConfig.templates;
609
+ if (templates === void 0) return null;
610
+ if (!Array.isArray(templates)) throw new OrgManifestSchemaError("createConfig.templates must be an array", packageName);
611
+ if (templates.length === 0) return null;
612
+ return validateTemplateEntries(templates, "createConfig.templates", (message) => new OrgManifestSchemaError(message, packageName), (entry, index) => validateEntry(entry, index, packageName));
734
613
  }
735
614
  /**
736
- * Replace characters that are illegal in Windows path segments
737
- * (`\ / : * ? " < > |` plus the IPv6 bracket pair `[ ]`). The host
738
- * comes from `new URL(...).host` which can carry a port (`:4873`) or
739
- * IPv6 literal (`[::1]`); both end up in the cache path otherwise.
615
+ * Schema-level failure for `create.templates` in `vite.config.ts`. A misconfigured
616
+ * local template should surface clearly rather than silently disappear.
740
617
  */
741
- function sanitizeHostForPath(host) {
742
- return host.replaceAll(/[\\/:*?"<>|[\]]/g, "_");
743
- }
618
+ var CreateConfigSchemaError = class extends Error {
619
+ constructor(message) {
620
+ super(message);
621
+ this.name = "CreateConfigSchemaError";
622
+ }
623
+ };
744
624
  /**
745
- * Cache extracted tarballs under `<host>/<scope>/create/<version>` so two
746
- * repos resolving the same `<scope>@<version>` through different registries
747
- * (via `.npmrc` scope mappings) don't share a cache slot. The registry
748
- * guarantees `manifest.tarballUrl` is a valid URL, so any parse failure
749
- * here is a real bug worth surfacing.
625
+ * Validate `create.templates` from `vite.config.ts`. Returns `[]` when the field
626
+ * is absent or an empty array; throws {@link CreateConfigSchemaError} when present
627
+ * but malformed.
750
628
  */
751
- function getExtractionDir(manifest) {
752
- const { host } = new URL(manifest.tarballUrl);
753
- return path.join(getCacheRoot(), sanitizeHostForPath(host), manifest.scope, "create", manifest.version);
754
- }
755
- function parseIntegrity(integrity) {
756
- const match = integrity.split(/\s+/)[0].match(/^(sha\d+)-(.+)$/);
757
- if (!match) return null;
758
- return {
759
- algorithm: match[1],
760
- expected: match[2]
761
- };
629
+ function validateCreateTemplates(templates) {
630
+ if (templates === void 0) return [];
631
+ if (!Array.isArray(templates)) throw new CreateConfigSchemaError("create.templates must be an array");
632
+ const makeError = (message) => new CreateConfigSchemaError(message);
633
+ return validateTemplateEntries(templates, "create.templates", makeError, (entry, index) => {
634
+ const validated = validateTemplateEntry(entry, index, "create.templates", makeError);
635
+ if (validated.name.startsWith("vite:")) throw makeError(`create.templates[${index}].name uses the reserved \`vite:\` prefix`);
636
+ return validated;
637
+ });
762
638
  }
763
- function verifyIntegrity(bytes, integrity) {
764
- if (!integrity) return;
765
- const parsed = parseIntegrity(integrity);
766
- if (!parsed) return;
767
- const hash = createHash(parsed.algorithm);
768
- hash.update(bytes);
769
- const actual = hash.digest("base64");
770
- if (actual !== parsed.expected) throw new Error(`integrity check failed: expected ${integrity}, got ${parsed.algorithm}-${actual}`);
639
+ async function fetchPackument(scope, packageName) {
640
+ const response = await fetchNpmResource(`${getNpmRegistry(scope)}/${packageName}`, {
641
+ headers: { accept: "application/json" },
642
+ timeoutMs: 5e3
643
+ });
644
+ if (response.status === 404) return null;
645
+ if (!response.ok) throw new Error(`npm registry responded with ${response.status} for ${packageName}`);
646
+ return await response.json();
771
647
  }
772
- const MAX_TARBALL_BYTES = 50 * 1024 * 1024;
773
- async function downloadTarball(url) {
774
- const response = await fetchNpmResource(url, { timeoutMs: 3e4 });
775
- if (!response.ok) throw new Error(`failed to download tarball (${response.status}): ${url}`);
776
- const contentLength = Number(response.headers.get("content-length"));
777
- if (Number.isFinite(contentLength) && contentLength > MAX_TARBALL_BYTES) throw new Error(`tarball exceeds ${MAX_TARBALL_BYTES} byte size limit: ${url}`);
778
- const reader = response.body?.getReader();
779
- if (!reader) throw new Error(`tarball response has no body: ${url}`);
780
- const chunks = [];
781
- let total = 0;
782
- while (true) {
783
- const { done, value } = await reader.read();
784
- if (done) break;
785
- total += value.byteLength;
786
- if (total > MAX_TARBALL_BYTES) {
787
- await reader.cancel();
788
- throw new Error(`tarball exceeds ${MAX_TARBALL_BYTES} byte size limit: ${url}`);
789
- }
790
- chunks.push(value);
791
- }
792
- const bytes = new Uint8Array(total);
793
- let offset = 0;
794
- for (const chunk of chunks) {
795
- bytes.set(chunk, offset);
796
- offset += chunk.byteLength;
648
+ /**
649
+ * Fetch `@scope/create` from the npm registry and parse its `createConfig.templates`
650
+ * manifest.
651
+ *
652
+ * Returns `null` when:
653
+ * - the package does not exist on the registry (404), or
654
+ * - the package exists but has no `createConfig.templates` field
655
+ *
656
+ * When the packument version metadata lacks `createConfig` entirely, the
657
+ * published tarball's package.json is consulted before giving up — some
658
+ * registries (GitHub Packages among them) strip custom fields from version
659
+ * metadata while preserving the tarball byte-for-byte.
660
+ *
661
+ * Throws when:
662
+ * - the `createConfig.templates` field is present but malformed (`OrgManifestSchemaError`), or
663
+ * - the registry request fails for any non-404 reason
664
+ *
665
+ * `requestedVersion` pins the lookup to a specific `versions[...]` entry
666
+ * (equivalent to `vp create @scope@1.2.3`); omit it to resolve `dist-tags.latest`.
667
+ */
668
+ async function readOrgManifest(scope, requestedVersion) {
669
+ if (!scope.startsWith("@")) return null;
670
+ const packageName = `${scope}/create`;
671
+ const packument = await fetchPackument(scope, packageName);
672
+ if (!packument) return null;
673
+ let resolvedVersion;
674
+ if (requestedVersion) {
675
+ resolvedVersion = packument["dist-tags"]?.[requestedVersion] ?? (packument.versions?.[requestedVersion] ? requestedVersion : void 0);
676
+ if (!resolvedVersion) throw new OrgManifestSchemaError(`version "${requestedVersion}" not found (known tags: ${Object.keys(packument["dist-tags"] ?? {}).join(", ") || "none"})`, packageName);
677
+ } else {
678
+ resolvedVersion = packument["dist-tags"]?.latest;
679
+ if (!resolvedVersion) return null;
797
680
  }
798
- return bytes;
681
+ const meta = packument.versions?.[resolvedVersion];
682
+ if (!meta) return null;
683
+ let templates = validateManifest(meta, packageName);
684
+ if (!templates && meta.createConfig === void 0 && meta.dist?.tarball) templates = validateManifest(await readPackageJsonFromTarball(meta.dist.tarball, meta.dist.integrity).catch(() => null), packageName);
685
+ if (!templates) return null;
686
+ if (!meta.dist?.tarball) throw new OrgManifestSchemaError(`missing dist.tarball for ${resolvedVersion}`, packageName);
687
+ return {
688
+ scope,
689
+ packageName,
690
+ version: resolvedVersion,
691
+ tarballUrl: meta.dist.tarball,
692
+ integrity: meta.dist.integrity,
693
+ templates
694
+ };
799
695
  }
800
- const STAGING_SUFFIX_PREFIX = ".tmp-";
801
696
  /**
802
- * Parse a tar entry's stored mode (always octal) into the numeric
803
- * permission bits (low 9 bits `rwxrwxrwx`). Returns `undefined` when
804
- * the mode is missing or unparsable so the caller leaves the file with
805
- * its default (umask-derived) permissions instead of downgrading.
697
+ * Apply the in-monorepo filter rule from the RFC: entries with
698
+ * `monorepo: true` are hidden when the command is invoked inside an
699
+ * existing monorepo, mirroring `initial-template-options.ts:9-31`.
806
700
  */
807
- function parseEntryMode(raw) {
808
- if (!raw) return;
809
- const parsed = Number.parseInt(raw, 8);
810
- if (!Number.isFinite(parsed)) return;
811
- return parsed & 511;
701
+ function filterManifestForContext(templates, isMonorepo) {
702
+ if (!isMonorepo) return [...templates];
703
+ return templates.filter((entry) => !entry.monorepo);
704
+ }
705
+ //#endregion
706
+ //#region src/create/templates/types.ts
707
+ const LibraryTemplateRepo = "github:sxzz/tsdown-templates/vite-plus";
708
+ const BuiltinTemplate = {
709
+ generator: "vite:generator",
710
+ monorepo: "vite:monorepo",
711
+ application: "vite:application",
712
+ library: "vite:library"
713
+ };
714
+ const TemplateType = {
715
+ builtin: "builtin",
716
+ bingo: "bingo",
717
+ remote: "remote",
718
+ bundled: "bundled"
719
+ };
720
+ //#endregion
721
+ //#region src/create/discovery.ts
722
+ function isGitHubUrl(templateName) {
723
+ return templateName.startsWith("https://github.com/") || templateName.startsWith("github:") || templateName.includes("github.com/");
724
+ }
725
+ function parseGitHubUrl(url) {
726
+ if (url.startsWith("github:")) return url.slice(7);
727
+ const match = url.match(/github\.com\/([^/]+\/[^/]+)/);
728
+ if (match) return match[1].replace(/\.git$/, "");
729
+ return null;
730
+ }
731
+ function inferGitHubRepoName(templateName) {
732
+ const degitPath = parseGitHubUrl(templateName);
733
+ if (!degitPath) return null;
734
+ return degitPath.split("/").pop() || null;
735
+ }
736
+ function localTemplateDir(workspaceInfo, templateName) {
737
+ if (isRelativePath(templateName)) return templateName.replace(/^\.\//, "");
738
+ return workspaceInfo.packages.find((pkg) => pkg.name === templateName)?.path;
739
+ }
740
+ function resolveLocalBinPath(localPackagePath, packageName, bin) {
741
+ if (!bin) return;
742
+ if (typeof bin === "string") return path.join(localPackagePath, bin);
743
+ const entries = Object.entries(bin);
744
+ if (entries.length === 0) return;
745
+ if (entries.length === 1) return path.join(localPackagePath, entries[0][1]);
746
+ const unscopedName = packageName.slice(packageName.lastIndexOf("/") + 1);
747
+ const preferred = bin[packageName] ?? bin[unscopedName];
748
+ if (preferred) return path.join(localPackagePath, preferred);
749
+ throw new Error(`Local template package "${packageName}" defines multiple "bin" entries (${entries.map(([name]) => name).join(", ")}); add a "bin" entry named "${packageName}" so the template entry is unambiguous`);
750
+ }
751
+ function discoverTemplate(templateName, templateArgs, workspaceInfo, interactive, bundledLocalPath, skipShorthand, localTemplate) {
752
+ const envs = prependToPathToEnvs(workspaceInfo.downloadPackageManager.binPrefix, { ...process.env });
753
+ const parentDir = inferParentDir(templateName, workspaceInfo, localTemplate);
754
+ if (bundledLocalPath) return {
755
+ command: "",
756
+ args: [...templateArgs],
757
+ envs,
758
+ type: TemplateType.bundled,
759
+ parentDir,
760
+ interactive,
761
+ localPath: bundledLocalPath
762
+ };
763
+ if (templateName.startsWith("vite:")) return {
764
+ command: templateName,
765
+ args: [...templateArgs],
766
+ envs,
767
+ type: TemplateType.builtin,
768
+ parentDir,
769
+ interactive
770
+ };
771
+ if (isGitHubUrl(templateName)) {
772
+ const degitPath = parseGitHubUrl(templateName);
773
+ if (degitPath) return {
774
+ command: "degit",
775
+ args: [degitPath, ...templateArgs],
776
+ envs,
777
+ type: TemplateType.remote,
778
+ parentDir,
779
+ interactive
780
+ };
781
+ }
782
+ if (localTemplate) {
783
+ const localDir = localTemplateDir(workspaceInfo, templateName);
784
+ if (!localDir) throw new Error(`Local template "${templateName}" does not match any workspace package; update the \`create.templates\` entry in vite.config.ts`);
785
+ const localPackagePath = path.join(workspaceInfo.rootDir, localDir);
786
+ const packageJsonPath = path.join(localPackagePath, "package.json");
787
+ if (!fs.existsSync(packageJsonPath)) throw new Error(`Local template "${templateName}" has no package.json, so it cannot be run as a template`);
788
+ const pkg = readJsonFile(packageJsonPath);
789
+ const binPath = resolveLocalBinPath(localPackagePath, pkg.name ?? templateName, pkg.bin);
790
+ if (!binPath) throw new Error(`Local template "${templateName}" has no "bin" entry in its package.json, so it cannot be run as a template`);
791
+ const args = [binPath, ...templateArgs];
792
+ let type = TemplateType.remote;
793
+ if (isBingoTemplate(pkg)) {
794
+ type = TemplateType.bingo;
795
+ args.push("--skip-requests");
796
+ }
797
+ return {
798
+ command: "node",
799
+ args,
800
+ envs,
801
+ type,
802
+ parentDir,
803
+ interactive
804
+ };
805
+ }
806
+ return {
807
+ command: skipShorthand ? templateName : expandCreateShorthand(templateName),
808
+ args: [...templateArgs],
809
+ envs,
810
+ type: TemplateType.remote,
811
+ parentDir,
812
+ interactive
813
+ };
812
814
  }
813
815
  /**
814
- * Strip the `package/` prefix from an `npm pack` tarball entry. Returns
815
- * `null` for entries to skip (root dir, PaxHeader, anything outside
816
- * `package/`).
816
+ * Expand shorthand template names to their full `create-*` package names.
817
+ *
818
+ * This follows the same convention as `npm create` / `pnpm create`:
819
+ * - `vite` → `create-vite`
820
+ * - `vite@latest` → `create-vite@latest`
821
+ *
822
+ * Special cases for packages where the convention doesn't work:
823
+ * - `nitro` → `create-nitro-app` (create-nitro is abandoned)
824
+ * - `svelte` → `sv`
825
+ * - `@tanstack/start` → `@tanstack/cli` (@tanstack/create-start is deprecated)
826
+ *
827
+ * Skips expansion for:
828
+ * - Builtin templates (`vite:*`)
829
+ * - GitHub URLs
830
+ * - Local paths (`./`, `../`, `/`)
831
+ * - Names already starting with `create-` (or `@scope/create-`)
817
832
  */
818
- function normalizeEntryName(rawName) {
819
- const name = rawName.replace(/^\.\//, "").replace(/\\/g, "/");
820
- if (!name || name === "package" || name === "package/") return null;
821
- if (name.startsWith("PaxHeader/") || name.includes("/PaxHeader/")) return null;
822
- if (!name.startsWith("package/")) return null;
823
- return name.slice(8);
824
- }
825
- async function extractTarballTo(bytes, destDir) {
826
- const entries = await parseTarGzip(bytes);
827
- const stagingDir = `${destDir}${STAGING_SUFFIX_PREFIX}${process.pid}-${Date.now()}`;
828
- await fs.promises.mkdir(stagingDir, { recursive: true });
829
- const resolvedStaging = path.resolve(stagingDir);
830
- try {
831
- for (const entry of entries) {
832
- const relativeName = normalizeEntryName(entry.name);
833
- if (relativeName === null) continue;
834
- const targetPath = path.join(stagingDir, relativeName);
835
- const resolvedTarget = path.resolve(targetPath);
836
- if (resolvedTarget !== resolvedStaging && !resolvedTarget.startsWith(`${resolvedStaging}${path.sep}`)) throw new Error(`tarball entry escapes extraction root: ${entry.name}`);
837
- if (entry.type === "directory" || relativeName.endsWith("/")) {
838
- await fs.promises.mkdir(targetPath, { recursive: true });
839
- continue;
840
- }
841
- await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
842
- const data = entry.data ?? /* @__PURE__ */ new Uint8Array(0);
843
- await fs.promises.writeFile(targetPath, data);
844
- const mode = parseEntryMode(entry.attrs?.mode);
845
- if (mode !== void 0) await fs.promises.chmod(targetPath, mode);
846
- }
847
- try {
848
- await fs.promises.rename(stagingDir, destDir);
849
- } catch (error) {
850
- const code = error.code;
851
- if ((code === "ENOTEMPTY" || code === "EEXIST") && fs.existsSync(path.join(destDir, "package.json"))) {
852
- await fs.promises.rm(stagingDir, {
853
- recursive: true,
854
- force: true
855
- }).catch(() => {});
856
- return;
857
- }
858
- throw error;
833
+ function expandCreateShorthand(templateName) {
834
+ if (templateName.includes(":")) return templateName;
835
+ if (isGitHubUrl(templateName)) return templateName;
836
+ if (templateName.startsWith("./") || templateName.startsWith("../") || templateName.startsWith("/")) return templateName;
837
+ if (templateName.startsWith("@")) {
838
+ const slashIndex = templateName.indexOf("/");
839
+ if (slashIndex === -1) {
840
+ const atIndex = templateName.indexOf("@", 1);
841
+ return `${atIndex === -1 ? templateName : templateName.slice(0, atIndex)}/create${atIndex === -1 ? "" : templateName.slice(atIndex)}`;
859
842
  }
860
- } catch (error) {
861
- await fs.promises.rm(stagingDir, {
862
- recursive: true,
863
- force: true
864
- }).catch(() => {});
865
- throw error;
843
+ const scope = templateName.slice(0, slashIndex);
844
+ const rest = templateName.slice(slashIndex + 1);
845
+ const atIndex = rest.indexOf("@");
846
+ const name = atIndex === -1 ? rest : rest.slice(0, atIndex);
847
+ const version = atIndex === -1 ? "" : rest.slice(atIndex);
848
+ if (name.startsWith("create-")) return templateName;
849
+ if (scope === "@tanstack" && name === "start") return `@tanstack/cli${version}`;
850
+ return `${scope}/create-${name}${version}`;
866
851
  }
852
+ const atIndex = templateName.indexOf("@");
853
+ const name = atIndex === -1 ? templateName : templateName.slice(0, atIndex);
854
+ const version = atIndex === -1 ? "" : templateName.slice(atIndex);
855
+ if (name.startsWith("create-")) return templateName;
856
+ if (name === "nitro") return `create-nitro-app${version}`;
857
+ if (name === "svelte") return `sv${version}`;
858
+ return `create-${name}${version}`;
867
859
  }
868
- const STAGING_STALE_MS = 1440 * 60 * 1e3;
869
- /**
870
- * Remove `<destDir>.tmp-*` siblings left behind by a previous crash so
871
- * repeated aborts don't accumulate orphaned staging trees. Only deletes
872
- * entries whose mtime is older than 24 hours — a concurrent `vp create`
873
- * that's still actively extracting will always be younger than that, so
874
- * the age gate keeps this safe to run at the top of every extract.
875
- */
876
- async function cleanupStaleStagingDirs(destDir) {
877
- const parent = path.dirname(destDir);
878
- const prefix = `${path.basename(destDir)}${STAGING_SUFFIX_PREFIX}`;
879
- let entries;
880
- try {
881
- entries = await fs.promises.readdir(parent);
882
- } catch {
883
- return;
860
+ function inferParentDir(templateName, workspaceInfo, localTemplate = false) {
861
+ if (workspaceInfo.parentDirs.length === 0) return;
862
+ const localDir = localTemplate ? localTemplateDir(workspaceInfo, templateName) : void 0;
863
+ if (localDir) {
864
+ const ownParentDir = path.dirname(localDir);
865
+ if (workspaceInfo.parentDirs.includes(ownParentDir)) return ownParentDir;
884
866
  }
885
- const cutoff = Date.now() - STAGING_STALE_MS;
886
- await Promise.all(entries.filter((name) => name.startsWith(prefix)).map(async (name) => {
887
- const fullPath = path.join(parent, name);
888
- try {
889
- if ((await fs.promises.stat(fullPath)).mtimeMs < cutoff) await fs.promises.rm(fullPath, {
890
- recursive: true,
891
- force: true
892
- });
893
- } catch {}
894
- }));
867
+ let rule = /app/i;
868
+ if (templateName === BuiltinTemplate.library) rule = /lib|component|package/i;
869
+ else if (templateName === BuiltinTemplate.generator) rule = /generator|tool/i;
870
+ for (const parentDir of workspaceInfo.parentDirs) if (rule.test(parentDir)) return parentDir;
871
+ }
872
+ //#endregion
873
+ //#region src/create/initial-template-options.ts
874
+ function getInitialTemplateOptions(isMonorepo, templates = []) {
875
+ return [
876
+ ...!isMonorepo ? [{
877
+ label: "Vite+ Monorepo",
878
+ value: BuiltinTemplate.monorepo,
879
+ hint: "Create a new Vite+ monorepo project"
880
+ }] : [],
881
+ {
882
+ label: "Vite+ Application",
883
+ value: BuiltinTemplate.application,
884
+ hint: "Create vite applications"
885
+ },
886
+ {
887
+ label: "Vite+ Library",
888
+ value: BuiltinTemplate.library,
889
+ hint: "Create vite libraries"
890
+ },
891
+ ...isMonorepo ? templates.map((entry) => ({
892
+ label: entry.name,
893
+ value: entry.name,
894
+ hint: entry.description
895
+ })) : []
896
+ ];
895
897
  }
898
+ //#endregion
899
+ //#region src/create/org-picker.ts
900
+ const ORG_PICKER_CANCEL = Symbol("org-picker-cancel");
901
+ const ORG_PICKER_BUILTIN_ESCAPE = Symbol("org-picker-builtin-escape");
902
+ const ESCAPE_HATCH = Symbol("builtin-escape");
896
903
  /**
897
- * Ensure the `@org/create` package tarball for the given manifest has been
898
- * downloaded and extracted locally. Returns the absolute path to the
899
- * extracted package root (i.e. the directory that contains
900
- * `package.json`).
904
+ * Render the interactive picker for an org manifest. Always appends a
905
+ * trailing "Vite+ built-in templates" escape-hatch entry.
901
906
  *
902
- * Idempotent: subsequent calls for the same `<scope, version>` reuse the
903
- * cached extraction. Concurrent calls race on the final rename; the loser
904
- * cleans up and returns the existing directory.
907
+ * Context-filters entries with `monorepo: true` when running inside an
908
+ * existing monorepo, mirroring `initial-template-options.ts:9-31`.
909
+ *
910
+ * Returns `ORG_PICKER_BUILTIN_ESCAPE` when the escape hatch is selected,
911
+ * or `ORG_PICKER_CANCEL` when the user hits Ctrl-C.
905
912
  */
906
- async function ensureOrgPackageExtracted(manifest) {
907
- const extractedRoot = getExtractionDir(manifest);
908
- if (fs.existsSync(path.join(extractedRoot, "package.json"))) return extractedRoot;
909
- const parent = path.dirname(extractedRoot);
910
- await fs.promises.mkdir(parent, { recursive: true });
911
- await cleanupStaleStagingDirs(extractedRoot);
912
- const bytes = await downloadTarball(manifest.tarballUrl);
913
- verifyIntegrity(bytes, manifest.integrity);
914
- await extractTarballTo(bytes, extractedRoot);
915
- return extractedRoot;
913
+ async function pickOrgTemplate(manifest, opts) {
914
+ const filtered = filterManifestForContext(manifest.templates, opts.isMonorepo);
915
+ if (filtered.length === 0) return ORG_PICKER_BUILTIN_ESCAPE;
916
+ const escapeValue = `__vp_builtin_escape__::${randomUUID()}`;
917
+ const lookup = /* @__PURE__ */ new Map();
918
+ const options = filtered.map((entry) => {
919
+ lookup.set(entry.name, entry);
920
+ return {
921
+ value: entry.name,
922
+ label: entry.name,
923
+ hint: entry.description
924
+ };
925
+ });
926
+ lookup.set(escapeValue, ESCAPE_HATCH);
927
+ const builtinHint = opts.isMonorepo ? "Use defaults (application / library)" : "Use defaults (monorepo / application / library)";
928
+ options.push({
929
+ value: escapeValue,
930
+ label: "Vite+ built-in templates",
931
+ hint: builtinHint
932
+ });
933
+ const picked = await select({
934
+ message: `Pick a template from ${manifest.scope}`,
935
+ options
936
+ });
937
+ if (isCancel(picked)) return ORG_PICKER_CANCEL;
938
+ const found = lookup.get(picked);
939
+ if (found === ESCAPE_HATCH) return ORG_PICKER_BUILTIN_ESCAPE;
940
+ if (!found) throw new Error(`org-picker: prompts.select returned an unregistered value: ${picked}`);
941
+ return {
942
+ kind: "entry",
943
+ entry: found
944
+ };
916
945
  }
917
946
  /**
918
- * Resolve a manifest entry's relative `./...` path against an already-
919
- * extracted package root, rejecting any path that escapes the root (via
920
- * `..` walks or an absolute specifier).
921
- *
922
- * Existence is NOT checked here — the subsequent `copyDir` surfaces any
923
- * missing-directory error with a clearer errno.
947
+ * Render the manifest as a plain-text table for the `--no-interactive`
948
+ * error output. Fixed column order so AI agents and scripts can recover
949
+ * available template names without a `--json` flag.
924
950
  */
925
- function resolveBundledPath(extractedRoot, relativePath) {
926
- if (path.isAbsolute(relativePath)) throw new Error(`bundled template path must be relative, got ${relativePath}`);
927
- const resolvedRoot = path.resolve(extractedRoot);
928
- const resolvedTarget = path.resolve(extractedRoot, relativePath);
929
- if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`)) throw new Error(`bundled template path escapes the package root: ${relativePath}`);
930
- return resolvedTarget;
951
+ function formatManifestTable(manifest, isMonorepo) {
952
+ const visible = filterManifestForContext(manifest.templates, isMonorepo);
953
+ const filteredCount = manifest.templates.length - visible.length;
954
+ const nameWidth = Math.max(4, ...visible.map((entry) => entry.name.length));
955
+ const descWidth = Math.max(11, ...visible.map((entry) => entry.description.length));
956
+ const lines = [];
957
+ lines.push(` ${"NAME".padEnd(nameWidth)} ${"DESCRIPTION".padEnd(descWidth)} TEMPLATE`);
958
+ for (const entry of visible) lines.push(` ${entry.name.padEnd(nameWidth)} ${entry.description.padEnd(descWidth)} ${entry.template}`);
959
+ return {
960
+ lines,
961
+ filteredCount
962
+ };
931
963
  }
932
964
  //#endregion
933
965
  //#region ../../node_modules/.pnpm/validate-npm-package-name@7.0.2/node_modules/validate-npm-package-name/lib/builtin-modules.json