@defold-typescript/cli 0.5.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/dispatch.ts CHANGED
@@ -2,10 +2,17 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import type { RegistryTarget } from "./api-registry";
4
4
  import { CURRENT_STABLE_SURFACE_ID, selectApiSurface } from "./api-surface";
5
+ import {
6
+ type DefoldIo,
7
+ defaultDefoldIo,
8
+ isDefoldSubcommand,
9
+ runDefoldCommand,
10
+ } from "./bob-command";
5
11
  import { runBuild } from "./build";
6
12
  import { readCliVersion } from "./cli-version";
7
13
  import { readDefoldVersionPin, resolveDefoldVersion } from "./defold-version";
8
14
  import { runInit } from "./init";
15
+ import { installHint } from "./install-reminder";
9
16
  import { renderResult } from "./json-output";
10
17
  import {
11
18
  ensureMaterializedReference,
@@ -14,7 +21,9 @@ import {
14
21
  type RefDocResolveOptions,
15
22
  resolveCurrentSurfaceGeneratedDir,
16
23
  } from "./materialize";
17
- import { detectScriptKinds, selectScriptKind } from "./script-kind";
24
+ import { runSetupDebug } from "./setup-debug";
25
+ import { applyWallSelection, currentWalledDirs, eligibleWalls } from "./wall";
26
+ import { type CheckboxPrompt, runWallInteractive } from "./wall-interactive";
18
27
  import {
19
28
  type RunWatchHandle,
20
29
  type RunWatchOptions,
@@ -37,9 +46,16 @@ export interface DispatchInternals {
37
46
  readonly resolveOpts?: RefDocResolveOptions;
38
47
  readonly refDocRegistry?: readonly RegistryTarget[];
39
48
  readonly cliVersion?: string;
49
+ readonly defoldIo?: Partial<DefoldIo>;
50
+ // `wall` takes its target directories as positionals (not a cwd path arg like
51
+ // the other commands), so tests inject the project root and TTY state here.
52
+ readonly cwd?: string;
53
+ readonly isTty?: boolean;
54
+ readonly wallCheckbox?: CheckboxPrompt;
40
55
  }
41
56
 
42
- const USAGE = "Usage: defold-typescript <init|build|watch> [path]\n";
57
+ const USAGE = "Usage: defold-typescript <init|build|watch|wall|setup-debug|defold> [path]\n";
58
+ const DEFOLD_USAGE = "Usage: defold-typescript defold <resolve|build|bundle> [path]\n";
43
59
 
44
60
  function parseDefoldVersionFlag(argv: string[]): { flag: string | undefined; rest: string[] } {
45
61
  let flag: string | undefined;
@@ -58,6 +74,45 @@ function parseDefoldVersionFlag(argv: string[]): { flag: string | undefined; res
58
74
  return { flag, rest };
59
75
  }
60
76
 
77
+ function parseScriptFlag(argv: string[]): { script: string | undefined; rest: string[] } {
78
+ let script: string | undefined;
79
+ const rest: string[] = [];
80
+ for (let i = 0; i < argv.length; i++) {
81
+ const arg = argv[i];
82
+ if (arg === "--script") {
83
+ script = argv[i + 1];
84
+ i++;
85
+ } else if (arg?.startsWith("--script=")) {
86
+ script = arg.slice("--script=".length);
87
+ } else if (arg !== undefined) {
88
+ rest.push(arg);
89
+ }
90
+ }
91
+ return { script, rest };
92
+ }
93
+
94
+ function parseValueFlag(
95
+ argv: string[],
96
+ name: string,
97
+ ): { value: string | undefined; rest: string[] } {
98
+ const long = `--${name}`;
99
+ const eq = `${long}=`;
100
+ let value: string | undefined;
101
+ const rest: string[] = [];
102
+ for (let i = 0; i < argv.length; i++) {
103
+ const arg = argv[i];
104
+ if (arg === long) {
105
+ value = argv[i + 1];
106
+ i++;
107
+ } else if (arg?.startsWith(eq)) {
108
+ value = arg.slice(eq.length);
109
+ } else if (arg !== undefined) {
110
+ rest.push(arg);
111
+ }
112
+ }
113
+ return { value, rest };
114
+ }
115
+
61
116
  function readProjectPin(cwd: string): string | undefined {
62
117
  const pkgPath = path.join(cwd, "package.json");
63
118
  if (!existsSync(pkgPath)) {
@@ -88,8 +143,24 @@ export function dispatch(
88
143
  }
89
144
 
90
145
  const force = argv.includes("--force");
91
- const { flag: defoldVersionFlag, rest: nonFlagArgs } = parseDefoldVersionFlag(argv);
92
- const positional = nonFlagArgs.filter((a) => a !== "--json" && a !== "--force");
146
+ const suppressInstallReminder = argv.includes("--suppress-install-reminder");
147
+ const wallRemove = argv.includes("--remove");
148
+ const wallList = argv.includes("--list");
149
+ const { flag: defoldVersionFlag, rest: afterVersionArgs } = parseDefoldVersionFlag(argv);
150
+ const { script: scriptFlag, rest: afterScriptArgs } = parseScriptFlag(afterVersionArgs);
151
+ const { value: javaFlag, rest: afterJavaArgs } = parseValueFlag(afterScriptArgs, "java");
152
+ const { value: buildServerFlag, rest: nonFlagArgs } = parseValueFlag(
153
+ afterJavaArgs,
154
+ "build-server",
155
+ );
156
+ const positional = nonFlagArgs.filter(
157
+ (a) =>
158
+ a !== "--json" &&
159
+ a !== "--force" &&
160
+ a !== "--suppress-install-reminder" &&
161
+ a !== "--remove" &&
162
+ a !== "--list",
163
+ );
93
164
  const [command, ...rest] = positional;
94
165
  const cwd = rest[0] ? path.resolve(rest[0]) : process.cwd();
95
166
 
@@ -103,7 +174,7 @@ export function dispatch(
103
174
 
104
175
  if (command === "init") {
105
176
  try {
106
- const { written, scriptKind } = runInit({ cwd, force });
177
+ const { written } = runInit({ cwd, force });
107
178
  if (json) {
108
179
  io.stdout.write(
109
180
  renderResult({
@@ -111,13 +182,16 @@ export function dispatch(
111
182
  written,
112
183
  defoldVersion: resolvedVersion,
113
184
  apiSurface,
114
- scriptKind,
185
+ installCommand: installHint(),
115
186
  }),
116
187
  );
117
188
  } else {
118
189
  io.stdout.write(
119
190
  `defold-typescript init: wrote ${written.length} files: ${written.join(", ")}\n`,
120
191
  );
192
+ if (!suppressInstallReminder) {
193
+ io.stdout.write(`Next: run \`${installHint()}\` to install dependencies.\n`);
194
+ }
121
195
  }
122
196
  return 0;
123
197
  } catch (err) {
@@ -131,10 +205,57 @@ export function dispatch(
131
205
  }
132
206
  }
133
207
 
208
+ if (command === "setup-debug") {
209
+ return (async (): Promise<number> => {
210
+ const result = await runSetupDebug({
211
+ cwd,
212
+ json,
213
+ ...(scriptFlag !== undefined ? { script: scriptFlag } : {}),
214
+ });
215
+ if (json) {
216
+ io.stdout.write(
217
+ renderResult(
218
+ result.ok
219
+ ? {
220
+ command: "setup-debug",
221
+ written: result.written,
222
+ actions: result.actions,
223
+ manualSteps: result.manualSteps,
224
+ ...(result.addedTo !== undefined ? { addedTo: result.addedTo } : {}),
225
+ removedFrom: result.removedFrom ?? [],
226
+ bootPath: result.bootPath ?? [],
227
+ }
228
+ : { command: "setup-debug", error: result.error ?? "setup-debug failed" },
229
+ ),
230
+ );
231
+ } else if (result.ok) {
232
+ io.stdout.write(
233
+ `defold-typescript setup-debug: wrote ${result.written.length} files: ${result.written.join(", ")}\n`,
234
+ );
235
+ if (result.addedTo !== undefined) {
236
+ io.stdout.write(`Debugger bootstrap added to: ${result.addedTo}\n`);
237
+ }
238
+ if (result.removedFrom !== undefined && result.removedFrom.length > 0) {
239
+ io.stdout.write(`Removed stale bootstrap from: ${result.removedFrom.join(", ")}\n`);
240
+ }
241
+ if (result.bootPath !== undefined && result.bootPath.length > 0) {
242
+ io.stdout.write(`Boot path: ${result.bootPath.join(" -> ")}\n`);
243
+ }
244
+ io.stdout.write("Remaining manual steps:\n");
245
+ for (const step of result.manualSteps) {
246
+ io.stdout.write(` - ${step}\n`);
247
+ }
248
+ } else {
249
+ io.stderr.write(`${result.error}\n`);
250
+ }
251
+ return result.ok ? 0 : 1;
252
+ })();
253
+ }
254
+
134
255
  if (command === "build") {
135
- const scriptKind = selectScriptKind(detectScriptKinds(cwd));
136
256
  const reportBuild = (written: readonly string[], materializedDir: string | null): number => {
137
257
  ensureMaterializedReference(cwd, materializedDir);
258
+ // walls are opt-in via the wall command
138
259
  if (json) {
139
260
  io.stdout.write(
140
261
  renderResult({
@@ -142,7 +263,6 @@ export function dispatch(
142
263
  written,
143
264
  defoldVersion: resolvedVersion,
144
265
  apiSurface,
145
- scriptKind,
146
266
  materializedSurface: materializedDir,
147
267
  }),
148
268
  );
@@ -176,7 +296,6 @@ export function dispatch(
176
296
  const { materializedDir } = await materializeRefDocSurface({
177
297
  cwd,
178
298
  surfaceId,
179
- scriptKind,
180
299
  ...(internals?.resolveOpts ? { resolveOpts: internals.resolveOpts } : {}),
181
300
  ...(internals?.refDocRegistry ? { registry: internals.refDocRegistry } : {}),
182
301
  });
@@ -200,7 +319,6 @@ export function dispatch(
200
319
  cwd,
201
320
  surface,
202
321
  sourceGeneratedDir,
203
- scriptKind,
204
322
  });
205
323
  return reportBuild(written, materializedDir);
206
324
  } catch (err) {
@@ -220,14 +338,13 @@ export function dispatch(
220
338
  const sourceGeneratedDir =
221
339
  internals?.sourceGeneratedDir ?? resolveCurrentSurfaceGeneratedDir();
222
340
  syncSurface = (): void => {
223
- const scriptKind = selectScriptKind(detectScriptKinds(cwd));
224
341
  const { materializedDir } = materializeApiSurface({
225
342
  cwd,
226
343
  surface,
227
344
  sourceGeneratedDir,
228
- scriptKind,
229
345
  });
230
346
  ensureMaterializedReference(cwd, materializedDir);
347
+ // walls are opt-in via the wall command
231
348
  };
232
349
  componentWatcherFactory = internals
233
350
  ? internals.componentWatcherFactory
@@ -243,6 +360,7 @@ export function dispatch(
243
360
  ...(internals?.debounceMs !== undefined ? { debounceMs: internals.debounceMs } : {}),
244
361
  ...(syncSurface ? { syncSurface } : {}),
245
362
  ...(componentWatcherFactory ? { componentWatcherFactory } : {}),
363
+ ...(json ? { json: true } : {}),
246
364
  };
247
365
  const handle = runWatch(watchOpts);
248
366
  if (internals) {
@@ -258,18 +376,15 @@ export function dispatch(
258
376
  };
259
377
 
260
378
  // A pinned ref-doc surface is generated on the fly, so it has no
261
- // `syncSurface`; narrow it once at startup the same way `build` does, then
262
- // start the watcher. A single detected kind drops the forbidden restricted
263
- // namespaces; mixed/none keeps the full surface. Live re-narrowing of
264
- // ref-doc surfaces stays deferred.
379
+ // `syncSurface`; generate it once at startup the same way `build` does, then
380
+ // start the watcher. The full surface materializes; walls are opt-in via the
381
+ // wall command.
265
382
  if (isRefDocSurface) {
266
383
  const surfaceId = surface.surfaceId as string;
267
- const scriptKind = selectScriptKind(detectScriptKinds(cwd));
268
384
  return (async (): Promise<number> => {
269
385
  const { materializedDir } = await materializeRefDocSurface({
270
386
  cwd,
271
387
  surfaceId,
272
- scriptKind,
273
388
  ...(internals?.resolveOpts ? { resolveOpts: internals.resolveOpts } : {}),
274
389
  ...(internals?.refDocRegistry ? { registry: internals.refDocRegistry } : {}),
275
390
  });
@@ -281,6 +396,137 @@ export function dispatch(
281
396
  return launchWatch();
282
397
  }
283
398
 
399
+ if (command === "wall") {
400
+ const wallCwd = internals?.cwd ?? process.cwd();
401
+ const dirs = rest;
402
+ const toJsonWall = (w: { dir: string; kind: string }): { dir: string; kind: string } => ({
403
+ dir: w.dir,
404
+ kind: w.kind,
405
+ });
406
+ const reportWalls = (walls: { dir: string; kind: string }[]): void => {
407
+ if (json) {
408
+ io.stdout.write(renderResult({ command: "wall", directoryWalls: walls.map(toJsonWall) }));
409
+ } else if (walls.length === 0) {
410
+ io.stdout.write("defold-typescript wall: no directories walled\n");
411
+ } else {
412
+ io.stdout.write(`defold-typescript wall: walled ${walls.map((w) => w.dir).join(", ")}\n`);
413
+ }
414
+ };
415
+
416
+ if (wallList) {
417
+ const current = currentWalledDirs(wallCwd);
418
+ const eligible = eligibleWalls(wallCwd);
419
+ const currentWalls = eligible.filter((w) => current.includes(w.dir));
420
+ if (json) {
421
+ io.stdout.write(
422
+ renderResult({
423
+ command: "wall",
424
+ directoryWalls: currentWalls.map(toJsonWall),
425
+ eligible: eligible.map(toJsonWall),
426
+ }),
427
+ );
428
+ } else {
429
+ io.stdout.write(
430
+ `defold-typescript wall: walled [${current.join(", ")}]; eligible [${eligible
431
+ .map((w) => w.dir)
432
+ .join(", ")}]\n`,
433
+ );
434
+ }
435
+ return 0;
436
+ }
437
+
438
+ if (dirs.length > 0) {
439
+ try {
440
+ const current = currentWalledDirs(wallCwd);
441
+ const desired = wallRemove
442
+ ? current.filter((d) => !dirs.includes(d))
443
+ : [...current, ...dirs];
444
+ reportWalls(applyWallSelection(wallCwd, desired));
445
+ return 0;
446
+ } catch (err) {
447
+ const message = err instanceof Error ? err.message : String(err);
448
+ if (json) {
449
+ io.stdout.write(renderResult({ command: "wall", error: message }));
450
+ } else {
451
+ io.stderr.write(`${message}\n`);
452
+ }
453
+ return 1;
454
+ }
455
+ }
456
+
457
+ // `--json` is machine-driven intent, so it never prompts even on a TTY.
458
+ const interactive = !json && (internals?.isTty ?? Boolean(process.stdout.isTTY));
459
+ if (!interactive) {
460
+ io.stderr.write(
461
+ "defold-typescript wall: no directory given; pass <dir> or run in a terminal for the interactive menu\n",
462
+ );
463
+ return 1;
464
+ }
465
+ return (async (): Promise<number> => {
466
+ try {
467
+ reportWalls(
468
+ await runWallInteractive(
469
+ wallCwd,
470
+ internals?.wallCheckbox ? { checkbox: internals.wallCheckbox } : {},
471
+ ),
472
+ );
473
+ return 0;
474
+ } catch (err) {
475
+ io.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
476
+ return 1;
477
+ }
478
+ })();
479
+ }
480
+
481
+ if (command === "defold") {
482
+ const subcommand = rest[0];
483
+ const defoldCwd = rest[1] ? path.resolve(rest[1]) : process.cwd();
484
+ if (!isDefoldSubcommand(subcommand)) {
485
+ io.stderr.write(DEFOLD_USAGE);
486
+ return 1;
487
+ }
488
+ const javaOverride = javaFlag ?? process.env.DEFOLD_JAVA;
489
+ const defoldIo: DefoldIo = { ...defaultDefoldIo(), ...internals?.defoldIo };
490
+ return (async (): Promise<number> => {
491
+ try {
492
+ const result = await runDefoldCommand({
493
+ cwd: defoldCwd,
494
+ subcommand,
495
+ ...(javaOverride !== undefined ? { java: javaOverride } : {}),
496
+ ...(buildServerFlag !== undefined ? { buildServer: buildServerFlag } : {}),
497
+ io: defoldIo,
498
+ });
499
+ if (json) {
500
+ io.stdout.write(
501
+ renderResult(
502
+ result.ok
503
+ ? { command: "defold", subcommand: result.subcommand, exitCode: result.exitCode }
504
+ : {
505
+ command: "defold",
506
+ subcommand: result.subcommand,
507
+ exitCode: result.exitCode,
508
+ error: `bob ${result.subcommand} exited with code ${result.exitCode}`,
509
+ },
510
+ ),
511
+ );
512
+ } else if (!result.ok) {
513
+ io.stderr.write(
514
+ `defold-typescript defold ${result.subcommand}: bob exited with code ${result.exitCode}\n`,
515
+ );
516
+ }
517
+ return result.exitCode;
518
+ } catch (err) {
519
+ const message = err instanceof Error ? err.message : String(err);
520
+ if (json) {
521
+ io.stdout.write(renderResult({ command: "defold", subcommand, error: message }));
522
+ } else {
523
+ io.stderr.write(`${message}\n`);
524
+ }
525
+ return 1;
526
+ }
527
+ })();
528
+ }
529
+
284
530
  io.stderr.write(USAGE);
285
531
  return 1;
286
532
  }
package/src/init.ts CHANGED
@@ -5,12 +5,7 @@ import type { ScriptHookName } from "@defold-typescript/types";
5
5
  import { DEBUG_LAUNCHER_SOURCE, debugLaunchConfig, VSCODE_LAUNCH_CONTENT } from "./debug-launcher";
6
6
  import { CURRENT_STABLE_DEFOLD_VERSION } from "./defold-version";
7
7
  import { mergeMiseToml } from "./mise-scaffold";
8
- import {
9
- detectScriptKinds,
10
- type ScriptKind,
11
- selectScriptKind,
12
- selectScriptKindEntrypoint,
13
- } from "./script-kind";
8
+ import { DEFAULT_TYPES_ENTRYPOINT } from "./script-kind";
14
9
 
15
10
  export interface RunInitOptions {
16
11
  readonly cwd: string;
@@ -19,7 +14,6 @@ export interface RunInitOptions {
19
14
 
20
15
  export interface RunInitResult {
21
16
  readonly written: string[];
22
- readonly scriptKind: ScriptKind | null;
23
17
  }
24
18
 
25
19
  const CONFLICTING_TS_CONFIGS = [
@@ -38,12 +32,30 @@ const TSCONFIG_COMPILER_OPTIONS = {
38
32
  skipLibCheck: true,
39
33
  };
40
34
 
41
- const GITIGNORE_LINES = ["src/**/*.ts.script", "src/**/*.ts.script.map"];
35
+ const GITIGNORE_LINES = [
36
+ "src/**/*.ts.script",
37
+ "src/**/*.ts.script.map",
38
+ "src/**/*.ts.gui_script",
39
+ "src/**/*.ts.gui_script.map",
40
+ "src/**/*.ts.render_script",
41
+ "src/**/*.ts.render_script.map",
42
+ "src/**/*.lua",
43
+ "src/**/*.lua.map",
44
+ ];
42
45
 
43
46
  const BIOME_JSON_CONTENT = {
44
47
  $schema: "https://biomejs.dev/schemas/2.4.15/schema.json",
45
48
  files: {
46
- includes: ["src/**/*.ts", "!**/dist", "!**/node_modules", "!**/*.ts.script"],
49
+ includes: [
50
+ "src/**/*.ts",
51
+ "!**/dist",
52
+ "!**/node_modules",
53
+ "!**/*.ts.script",
54
+ "!**/*.ts.gui_script",
55
+ "!**/*.ts.render_script",
56
+ "!src/**/*.lua",
57
+ "!src/**/*.lua.map",
58
+ ],
47
59
  },
48
60
  formatter: {
49
61
  enabled: true,
@@ -76,10 +88,17 @@ const BIOME_JSON_CONTENT = {
76
88
  };
77
89
 
78
90
  const VSCODE_EXTENSIONS_CONTENT = {
79
- recommendations: ["sumneko.lua", "astronachos.defold", "tomblind.local-lua-debugger-vscode"],
91
+ recommendations: ["tomblind.local-lua-debugger-vscode"],
80
92
  unwantedRecommendations: ["johnnymorganz.luau-lsp"],
81
93
  };
82
94
 
95
+ const MANAGED_RECOMMENDATIONS = [
96
+ "tomblind.local-lua-debugger-vscode",
97
+ "sumneko.lua",
98
+ "astronachos.defold",
99
+ ];
100
+ const MANAGED_UNWANTED = ["johnnymorganz.luau-lsp"];
101
+
83
102
  const VSCODE_SETTINGS_CONTENT = {
84
103
  "Lua.workspace.ignoreDir": ["src"],
85
104
  };
@@ -154,7 +173,7 @@ function inlineSnippetBody(factory: string, includeOnInput: boolean): string[] {
154
173
  return [
155
174
  `import { ${factory} } from "@defold-typescript/types";`,
156
175
  "",
157
- `export const script = ${factory}({`,
176
+ `export default ${factory}({`,
158
177
  ` // ${HOOK_COMMENTS.init}`,
159
178
  " init() {",
160
179
  " return { $0 };",
@@ -173,7 +192,7 @@ function typedSnippetBody(factory: string, includeOnInput: boolean): string[] {
173
192
  " $1",
174
193
  "};",
175
194
  "",
176
- `export const script = ${factory}<Self>({`,
195
+ `export default ${factory}<Self>({`,
177
196
  ` // ${HOOK_COMMENTS.init}`,
178
197
  " init(): Self {",
179
198
  " return { $0 };",
@@ -222,23 +241,21 @@ const VSCODE_SNIPPETS_CONTENT: Record<string, VscodeSnippet> = {
222
241
  },
223
242
  };
224
243
 
225
- const MAIN_TS_CONTENT = `export function init(): void {
226
- const start = vmath.vector3(0, 0, 0);
227
- msg.post("main:/hero", "spawn", { start });
228
- }
229
- `;
244
+ const MAIN_TS_CONTENT = `import { defineScript } from "@defold-typescript/types";
230
245
 
231
- const MAIN_SCRIPT_CONTENT = `function init(self) end
232
- function update(self, dt) end
233
- function on_message(self, message_id, message, sender) end
234
- function final(self) end
246
+ export default defineScript({
247
+ init() {
248
+ const start = vmath.vector3(0, 0, 0);
249
+ return { start };
250
+ },
251
+ });
235
252
  `;
236
253
 
237
254
  const MAIN_COLLECTION_CONTENT = `name: "main"
238
255
  scale_along_z: 0
239
256
  embedded_instances {
240
257
  id: "main"
241
- data: "components {\\n id: \\"main\\"\\n component: \\"/main/main.script\\"\\n}\\n"
258
+ data: "components {\\n id: \\"main\\"\\n component: \\"/src/main.ts.script\\"\\n}\\n"
242
259
  position { x: 0.0 y: 0.0 z: 0.0 }
243
260
  rotation { x: 0.0 y: 0.0 z: 0.0 w: 1.0 }
244
261
  scale3 { x: 1.0 y: 1.0 z: 1.0 }
@@ -270,8 +287,8 @@ function typesVersionSpec(): string {
270
287
  }
271
288
 
272
289
  // @defold-typescript/types (type-only, for the editor) and @defold-typescript/cli
273
- // (the local bin the managed `bunx --no-install defold-typescript` mise tasks
274
- // resolve) both ship into the consumer. The transpiler must NOT be a direct
290
+ // (the local bin the managed `bunx @defold-typescript/cli` mise tasks resolve
291
+ // inside an installed project) both ship into the consumer. The transpiler must NOT be a direct
275
292
  // consumer dep — it arrives transitively through the CLI. Pin both managed deps
276
293
  // to this CLI's own version so the coordinated-release set stays in lockstep.
277
294
  export const SCAFFOLD_DEV_DEPS: Record<string, string> = {
@@ -400,6 +417,34 @@ function unionStrings(existing: unknown, additions: readonly string[]): string[]
400
417
  return out;
401
418
  }
402
419
 
420
+ export function reconcileManagedList(
421
+ existing: unknown,
422
+ managed: readonly string[],
423
+ canonical: readonly string[],
424
+ ): string[] {
425
+ const managedSet = new Set(managed);
426
+ const canonicalSet = new Set(canonical);
427
+ const out: string[] = [];
428
+ const values = Array.isArray(existing)
429
+ ? existing.filter((value): value is string => typeof value === "string")
430
+ : [];
431
+ for (const value of values) {
432
+ if (out.includes(value)) {
433
+ continue;
434
+ }
435
+ if (managedSet.has(value) && !canonicalSet.has(value)) {
436
+ continue;
437
+ }
438
+ out.push(value);
439
+ }
440
+ for (const value of canonical) {
441
+ if (!out.includes(value)) {
442
+ out.push(value);
443
+ }
444
+ }
445
+ return out;
446
+ }
447
+
403
448
  function readVscodeJson(filePath: string): Record<string, unknown> | null {
404
449
  try {
405
450
  const parsed = parseJsonc(readFileSync(filePath, "utf8"));
@@ -417,15 +462,20 @@ function writeVscodeExtensions(cwd: string, written: string[]): void {
417
462
  if (existing === null) {
418
463
  return;
419
464
  }
420
- existing.recommendations = unionStrings(
465
+ const before = JSON.stringify(existing);
466
+ existing.recommendations = reconcileManagedList(
421
467
  existing.recommendations,
468
+ MANAGED_RECOMMENDATIONS,
422
469
  VSCODE_EXTENSIONS_CONTENT.recommendations,
423
470
  );
424
- existing.unwantedRecommendations = unionStrings(
471
+ existing.unwantedRecommendations = reconcileManagedList(
425
472
  existing.unwantedRecommendations,
473
+ MANAGED_UNWANTED,
426
474
  VSCODE_EXTENSIONS_CONTENT.unwantedRecommendations,
427
475
  );
428
- writeJson(filePath, existing);
476
+ if (JSON.stringify(existing) !== before) {
477
+ writeJson(filePath, existing);
478
+ }
429
479
  return;
430
480
  }
431
481
  mkdirSync(dir, { recursive: true });
@@ -509,16 +559,15 @@ function writeVscodeDebugLauncher(cwd: string, written: string[]): void {
509
559
  written.push(".vscode/defold-debug.ts");
510
560
  }
511
561
 
512
- function writeTsSurface(cwd: string, written: string[], force = false): ScriptKind | null {
562
+ function writeTsSurface(cwd: string, written: string[], force = false): void {
513
563
  mkdirSync(path.join(cwd, "src"), { recursive: true });
514
564
  writeFileSync(path.join(cwd, "src", "main.ts"), MAIN_TS_CONTENT);
515
565
  written.push("src/main.ts");
516
566
 
517
- const kinds = detectScriptKinds(cwd);
518
567
  const tsconfig = {
519
568
  compilerOptions: {
520
569
  ...TSCONFIG_COMPILER_OPTIONS,
521
- types: [selectScriptKindEntrypoint(kinds)],
570
+ types: [DEFAULT_TYPES_ENTRYPOINT],
522
571
  },
523
572
  include: ["src/**/*.ts"],
524
573
  };
@@ -561,8 +610,6 @@ function writeTsSurface(cwd: string, written: string[], force = false): ScriptKi
561
610
  writeVscodeSnippets(cwd, written);
562
611
  writeVscodeLaunch(cwd, written);
563
612
  writeVscodeDebugLauncher(cwd, written);
564
-
565
- return selectScriptKind(kinds);
566
613
  }
567
614
 
568
615
  export function runNewProjectInit(cwd: string, force = false): RunInitResult {
@@ -585,12 +632,10 @@ export function runNewProjectInit(cwd: string, force = false): RunInitResult {
585
632
  mkdirSync(path.join(cwd, "main"), { recursive: true });
586
633
  writeFileSync(path.join(cwd, "main", "main.collection"), MAIN_COLLECTION_CONTENT);
587
634
  written.push("main/main.collection");
588
- writeFileSync(path.join(cwd, "main", "main.script"), MAIN_SCRIPT_CONTENT);
589
- written.push("main/main.script");
590
635
 
591
- const scriptKind = writeTsSurface(cwd, written, force);
636
+ writeTsSurface(cwd, written, force);
592
637
 
593
- return { written, scriptKind };
638
+ return { written };
594
639
  }
595
640
 
596
641
  export function runInit(opts: RunInitOptions): RunInitResult {
@@ -611,6 +656,6 @@ export function runInit(opts: RunInitOptions): RunInitResult {
611
656
  }
612
657
 
613
658
  const written: string[] = [];
614
- const scriptKind = writeTsSurface(cwd, written, force);
615
- return { written, scriptKind };
659
+ writeTsSurface(cwd, written, force);
660
+ return { written };
616
661
  }
@@ -0,0 +1,18 @@
1
+ // The runner (`bunx`/`npx`/`pnpm dlx`/`yarn dlx`) sets `npm_config_user_agent`
2
+ // with the manager as its first token; when the bin is run directly the var is
3
+ // unset. A wrong guess only mis-advises — it never writes a lockfile — so the
4
+ // fallback to bun (the repo's primary manager) is safe.
5
+ export function installHint(env: NodeJS.ProcessEnv = process.env): string {
6
+ const agent = env.npm_config_user_agent ?? "";
7
+ const manager = agent.split("/")[0];
8
+ if (manager === "pnpm") {
9
+ return "pnpm install";
10
+ }
11
+ if (manager === "yarn") {
12
+ return "yarn install";
13
+ }
14
+ if (manager === "npm") {
15
+ return "npm install";
16
+ }
17
+ return "bun install";
18
+ }