@monimesl/dot-cli 0.1.0-alpha.19 → 0.1.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/cli.js +381 -261
  2. package/dist/cli.js.map +1 -1
  3. package/dist/{server-uIyYmYnp.js → server-B9-F93mS.js} +539 -464
  4. package/dist/server-B9-F93mS.js.map +1 -0
  5. package/dist/templates/showcase/dot.ts +2 -0
  6. package/dist/templates/showcase/pages/basics/bars/controller.ts +1 -1
  7. package/dist/templates/showcase/pages/basics/control-flow/controller.ts +1 -1
  8. package/dist/templates/showcase/pages/basics/events/controller.ts +4 -4
  9. package/dist/templates/showcase/pages/basics/events/view.dml +2 -2
  10. package/dist/templates/showcase/pages/basics/hooks/controller.ts +6 -0
  11. package/dist/templates/showcase/pages/basics/hooks/view.dml +5 -0
  12. package/dist/templates/showcase/pages/basics/navigation/controller.ts +7 -0
  13. package/dist/templates/showcase/pages/basics/navigation/state.ts +2 -0
  14. package/dist/templates/showcase/pages/basics/navigation/view.dml +11 -0
  15. package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +12 -0
  16. package/dist/templates/showcase/pages/basics/pick-genre/state.ts +3 -0
  17. package/dist/templates/showcase/pages/basics/pick-genre/view.dml +9 -0
  18. package/dist/templates/showcase/pages/components/carousel/controller.ts +1 -1
  19. package/dist/templates/showcase/pages/components/feedback/controller.ts +12 -2
  20. package/dist/templates/showcase/pages/components/feedback/state.ts +33 -0
  21. package/dist/templates/showcase/pages/components/feedback/view.dml +21 -1
  22. package/dist/templates/showcase/pages/components/forms/controller.ts +22 -22
  23. package/dist/templates/showcase/pages/components/forms/state.ts +3 -3
  24. package/dist/templates/showcase/pages/components/forms/view.dml +9 -2
  25. package/dist/templates/showcase/pages/components/icons/controller.ts +1 -1
  26. package/dist/templates/showcase/pages/components/images/controller.ts +3 -1
  27. package/dist/templates/showcase/pages/components/images/view.dml +13 -0
  28. package/dist/templates/showcase/pages/components/layout/controller.ts +2 -2
  29. package/dist/templates/showcase/pages/components/lists/controller.ts +1 -1
  30. package/dist/templates/showcase/pages/components/lists/view.dml +18 -0
  31. package/dist/templates/showcase/pages/components/money/controller.ts +6 -8
  32. package/dist/templates/showcase/pages/components/sheets/controller.ts +2 -2
  33. package/dist/templates/showcase/pages/components/tabs/controller.ts +22 -10
  34. package/dist/templates/showcase/pages/components/tabs/state.ts +1 -0
  35. package/dist/templates/showcase/pages/components/tabs/view.dml +4 -3
  36. package/dist/templates/showcase/pages/dash/cache/controller.ts +2 -2
  37. package/dist/templates/showcase/pages/dash/calls/controller.ts +7 -6
  38. package/dist/templates/showcase/pages/dash/calls/state.ts +1 -1
  39. package/dist/templates/showcase/pages/dash/calls/view.dml +3 -2
  40. package/dist/templates/showcase/pages/dash/contacts/controller.ts +30 -0
  41. package/dist/templates/showcase/pages/dash/contacts/state.ts +6 -0
  42. package/dist/templates/showcase/pages/dash/contacts/view.dml +33 -0
  43. package/dist/templates/showcase/pages/dash/files/controller.ts +15 -13
  44. package/dist/templates/showcase/pages/dash/files/state.ts +3 -6
  45. package/dist/templates/showcase/pages/dash/media/controller.ts +16 -13
  46. package/dist/templates/showcase/pages/dash/network/controller.ts +71 -16
  47. package/dist/templates/showcase/pages/dash/network/state.ts +10 -0
  48. package/dist/templates/showcase/pages/dash/network/view.dml +32 -4
  49. package/dist/templates/showcase/pages/dash/secrets/controller.ts +1 -1
  50. package/dist/templates/showcase/pages/dash/system/controller.ts +32 -5
  51. package/dist/templates/showcase/pages/dash/system/state.ts +6 -2
  52. package/dist/templates/showcase/pages/dash/system/view.dml +39 -1
  53. package/dist/templates/showcase/pages/dash/ui/controller.ts +23 -23
  54. package/dist/templates/showcase/pages/dash/ui/view.dml +1 -1
  55. package/dist/templates/showcase/pages/index/controller.ts +1 -1
  56. package/dist/templates/showcase/pages/index/state.ts +17 -10
  57. package/package.json +6 -6
  58. package/dist/server-uIyYmYnp.js.map +0 -1
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { parseArgs, styleText } from "node:util";
5
- import { CompileError, PAGES_DIR, build, installedRuntime, isDotPackage, loadProject } from "@monimesl/dot-compiler";
5
+ import { CompileError, DOT_FILE, DOT_JSON_FILE, PAGES_DIR, build, installedRuntime, isDotPackage, loadProject } from "@monimesl/dot-compiler";
6
6
  import { DEFAULT_PORT, startSimulator } from "@monimesl/dot-simulator";
7
7
  import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
8
8
  import { cp, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
@@ -21,11 +21,19 @@ var CommandError = class extends Error {
21
21
  };
22
22
  function describeError(error) {
23
23
  if (error instanceof CommandError || error instanceof CompileError || error instanceof DmlParseError || error instanceof ScriptParseError) return error.message;
24
- if (error.code?.startsWith("ERR_PARSE_ARGS")) {
24
+ if (isArgumentError(error)) {
25
25
  const [firstSentence] = error.message.split(". ");
26
26
  return `${firstSentence}. See dotctl --help.`;
27
27
  }
28
- return error instanceof Error ? error.stack ?? error.message : String(error);
28
+ if (error instanceof Error) return error.stack ?? error.message;
29
+ return String(error);
30
+ }
31
+ function isArgumentError(error) {
32
+ if (!(error instanceof Error) || !("code" in error)) return false;
33
+ return typeof error.code === "string" && error.code.startsWith("ERR_PARSE_ARGS");
34
+ }
35
+ function printFiles(files) {
36
+ for (const file of files) console.log(` ${relative(process.cwd(), file)}`);
29
37
  }
30
38
  function printWarnings({ warnings }) {
31
39
  for (const warning of warnings) console.warn(`warning: ${warning.message}`);
@@ -33,6 +41,26 @@ function printWarnings({ warnings }) {
33
41
  function printBuildTime(startedAt) {
34
42
  console.log(`Built in ${Math.round(performance.now() - startedAt)}ms`);
35
43
  }
44
+ function printSimulatorUrl(url) {
45
+ console.log(`\n Simulator running at: ${url}\n`);
46
+ }
47
+ //#endregion
48
+ //#region src/utils.ts
49
+ function findFolderWith(fileName, dir) {
50
+ let current = dir;
51
+ while (true) {
52
+ if (existsSync(join(current, fileName))) return current;
53
+ const parent = dirname(current);
54
+ if (parent === current) return;
55
+ current = parent;
56
+ }
57
+ }
58
+ function findDotRoot(dir) {
59
+ return findFolderWith(DOT_JSON_FILE, dir);
60
+ }
61
+ function resolveOutDir(root, outDir) {
62
+ return resolve(root, outDir ?? "dist");
63
+ }
36
64
  //#endregion
37
65
  //#region src/create/template.ts
38
66
  const TEMPLATES = ["starter", "showcase"];
@@ -60,8 +88,8 @@ const TEMPLATE_FILES = [
60
88
  ".env"
61
89
  ];
62
90
  const DOT_JSON_SCHEMA = "https://unpkg.com/@monimesl/dot-runtime/dot.schema.json";
63
- const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
64
- function projectFiles(options) {
91
+ const UNREGISTERED_DOT_ID = "<monime-Internal-Id>";
92
+ function generatedFiles(options) {
65
93
  return {
66
94
  "package.json": packageJson(options),
67
95
  "tsconfig.json": tsconfig(options),
@@ -72,7 +100,7 @@ function projectFiles(options) {
72
100
  }
73
101
  function packageJson({ packageName, dotVersion, workspace }) {
74
102
  const conditions = workspace ? " --conditions @monimesl/dot-source" : "";
75
- return json({
103
+ return formatJson({
76
104
  name: packageName,
77
105
  private: true,
78
106
  type: "module",
@@ -102,7 +130,7 @@ function tsconfig({ workspace }) {
102
130
  verbatimModuleSyntax: true
103
131
  };
104
132
  if (workspace) compilerOptions.customConditions = ["@monimesl/dot-source"];
105
- return json({
133
+ return formatJson({
106
134
  compilerOptions,
107
135
  include: [
108
136
  "dot.ts",
@@ -112,7 +140,6 @@ function tsconfig({ workspace }) {
112
140
  ]
113
141
  });
114
142
  }
115
- const UNREGISTERED_DOT_ID = "<monime-Internal-Id>";
116
143
  function dotJson({ name, dotPackage, template }) {
117
144
  const dot = {
118
145
  id: UNREGISTERED_DOT_ID,
@@ -125,7 +152,8 @@ function dotJson({ name, dotPackage, template }) {
125
152
  dot.permissions = {
126
153
  camera: "Take a photo for your concert ticket, or scan a ticket's QR code",
127
154
  photoLibrary: "Choose a photo for your concert ticket",
128
- files: "Attach your concert ticket"
155
+ files: "Attach your concert ticket",
156
+ contacts: "Choose a friend to send a ticket to"
129
157
  };
130
158
  dot.network = { hosts: [
131
159
  "itunes.apple.com",
@@ -133,7 +161,7 @@ function dotJson({ name, dotPackage, template }) {
133
161
  "httpbin.org"
134
162
  ] };
135
163
  }
136
- return json(dot);
164
+ return formatJson(dot);
137
165
  }
138
166
  const TEMPLATE_READMES = {
139
167
  starter: `It starts with one page, \`index\`. It shows a greeting from \`.env\` and a counter you
@@ -172,7 +200,7 @@ also lists the page in \`dot.ts\`. The dot opens on the first page in that list.
172
200
 
173
201
  \`dot.json\` describes the dot to Dash. Its \`package\` is a reverse domain name you own, such as
174
202
  \`com.mysalone.thing\`. You can't change it after publishing. To publish, register the dot with
175
- Monime first, and replace its \`id\`, \`<monime-Internal-Id>\`, with the ID Monime gives you.
203
+ Monime first, and replace its \`id\`, \`${UNREGISTERED_DOT_ID}\`, with the ID Monime gives you.
176
204
  \`network.hosts\` lists every host the dot sends requests to with \`dash.network\`. Requests to any
177
205
  other host fail. Your editor can complete the other fields from the schema the file points to.
178
206
 
@@ -180,7 +208,7 @@ other host fail. Your editor can complete the other fields from the schema the f
180
208
 
181
209
  Dash opens the dot unless it was built with a newer major version of \`@monimesl/dot-runtime\` than
182
210
  the one Dash has. A newer minor version can add Dash calls that an older Dash doesn't have yet.
183
- There, such a call fails with a \`HostError\` whose \`code\` is \`unsupported\`, so catch it and carry
211
+ There, such a call fails with a \`DashError\` whose \`code\` is \`unsupported\`, so catch it and carry
184
212
  on without the feature.
185
213
 
186
214
  ## .env
@@ -194,6 +222,9 @@ Never put a secret there. \`.env.development\` and \`.env.production\` override
194
222
  function suggestedDotPackage(packageName) {
195
223
  return `com.example.${packageName.slice(0, 63).replace(/-+$/, "")}`;
196
224
  }
225
+ function formatJson(value) {
226
+ return `${JSON.stringify(value, null, 2)}\n`;
227
+ }
197
228
  //#endregion
198
229
  //#region src/create/create.ts
199
230
  const DOT_PACKAGE_RULE = "Use lowercase words joined by dots, such as com.mysalone.thing.";
@@ -208,7 +239,7 @@ async function create({ name, dir, dotPackage, template = "starter" }) {
208
239
  const source = templateDir(template);
209
240
  await mkdir(root, { recursive: true });
210
241
  for (const path of TEMPLATE_FILES) await cp(join(source, path), join(root, path), { recursive: true });
211
- const generated = projectFiles({
242
+ const generated = generatedFiles({
212
243
  name,
213
244
  packageName,
214
245
  dotPackage,
@@ -229,17 +260,11 @@ function toDisplayName(dir) {
229
260
  return basename(resolve(dir)).split(/[-_\s]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
230
261
  }
231
262
  function isInDotWorkspace(dir) {
232
- let current = dirname(dir);
233
- while (true) {
234
- if (existsSync(join(current, "pnpm-workspace.yaml"))) {
235
- const runtimeManifest = join(current, "packages/runtime/package.json");
236
- if (!existsSync(runtimeManifest)) return false;
237
- return readPackageJson(runtimeManifest).name === "@monimesl/dot-runtime";
238
- }
239
- const parent = dirname(current);
240
- if (parent === current) return false;
241
- current = parent;
242
- }
263
+ const workspaceRoot = findFolderWith("pnpm-workspace.yaml", dirname(dir));
264
+ if (workspaceRoot === void 0) return false;
265
+ const runtimeManifest = join(workspaceRoot, "packages/runtime/package.json");
266
+ if (!existsSync(runtimeManifest)) return false;
267
+ return readPackageJson(runtimeManifest).name === "@monimesl/dot-runtime";
243
268
  }
244
269
  function templateDir(template) {
245
270
  const example = join(cliRoot(), "../../examples", template);
@@ -255,22 +280,12 @@ function cliRoot() {
255
280
  return dirname(createRequire(import.meta.url).resolve("@monimesl/dot-cli/package.json"));
256
281
  }
257
282
  //#endregion
258
- //#region src/dot-root.ts
259
- function findDotRoot(dir) {
260
- while (true) {
261
- if (existsSync(join(dir, "dot.json"))) return dir;
262
- const parent = dirname(dir);
263
- if (parent === dir) return;
264
- dir = parent;
265
- }
266
- }
267
- //#endregion
268
283
  //#region src/create/page.ts
269
284
  async function createPage(path, cwd) {
270
285
  if (!isPageId(path)) throw new CommandError(`"${path}" is not a page path. Use letters, digits, "-" and "_", with "/" between folders, such as "cart/checkout"`);
271
286
  const root = findDotRoot(resolve(cwd));
272
287
  if (root === void 0) throw new CommandError("There is no dot.json in this folder or any folder above it. Run this command inside a dot");
273
- const dir = join(root, "pages", path);
288
+ const dir = join(root, PAGES_DIR, path);
274
289
  if (existsSync(dir)) throw new CommandError(`pages/${path} already exists. Choose another path`);
275
290
  await mkdir(dir, { recursive: true });
276
291
  const files = [];
@@ -282,10 +297,10 @@ async function createPage(path, cwd) {
282
297
  return {
283
298
  dir,
284
299
  files,
285
- listing: await listPage(join(root, "dot.ts"), path)
300
+ listing: await addToDotPages(join(root, DOT_FILE), path)
286
301
  };
287
302
  }
288
- async function listPage(dotFile, path) {
303
+ async function addToDotPages(dotFile, path) {
289
304
  if (!existsSync(dotFile)) return "manual";
290
305
  const source = await readFile(dotFile, "utf8");
291
306
  let dot;
@@ -295,23 +310,25 @@ async function listPage(dotFile, path) {
295
310
  return "manual";
296
311
  }
297
312
  if (dot.pages.includes(path)) return "already";
298
- const updated = withPage(source, dot.lastPage, path);
313
+ const updated = insertPage(source, dot.lastPage, path);
299
314
  if (!listsPage(updated, dotFile, path)) return "manual";
300
315
  await writeFile(dotFile, updated);
301
316
  return "added";
302
317
  }
303
- function withPage(source, lastPage, path) {
318
+ function insertPage(source, lastPage, path) {
304
319
  const quote = source[lastPage.start] === "\"" ? "\"" : "'";
305
320
  const page = `${quote}${path}${quote}`;
321
+ const before = source.slice(0, lastPage.end);
322
+ const after = source.slice(lastPage.end);
306
323
  const lineStart = source.lastIndexOf("\n", lastPage.start - 1) + 1;
307
324
  const indent = source.slice(lineStart, lastPage.start);
308
- if (!/^\s*$/.test(indent)) return `${source.slice(0, lastPage.end)}, ${page}${source.slice(lastPage.end)}`;
309
- const trailingComma = /^\s*,/.exec(source.slice(lastPage.end));
325
+ if (!/^\s*$/.test(indent)) return `${before}, ${page}${after}`;
326
+ const trailingComma = /^\s*,/.exec(after);
310
327
  if (trailingComma) {
311
328
  const end = lastPage.end + trailingComma[0].length;
312
329
  return `${source.slice(0, end)}\n${indent}${page},${source.slice(end)}`;
313
330
  }
314
- return `${source.slice(0, lastPage.end)},\n${indent}${page}${source.slice(lastPage.end)}`;
331
+ return `${before},\n${indent}${page}${after}`;
315
332
  }
316
333
  function listsPage(source, dotFile, path) {
317
334
  try {
@@ -334,10 +351,10 @@ function isPackageManager(name) {
334
351
  function launchedBy(userAgent = process.env.npm_config_user_agent ?? "") {
335
352
  return PACKAGE_MANAGERS.find((name) => userAgent.startsWith(`${name}/`)) ?? "npm";
336
353
  }
337
- function installedManagers(isInstalled = onPath) {
354
+ function installedManagers(isInstalled = isOnPath) {
338
355
  return PACKAGE_MANAGERS.filter(isInstalled);
339
356
  }
340
- function onPath(name) {
357
+ function isOnPath(name) {
341
358
  return spawnSync(name, ["--version"], {
342
359
  stdio: "ignore",
343
360
  shell: process.platform === "win32"
@@ -366,16 +383,18 @@ async function loadDeveloperKey(path = defaultDeveloperKeyPath()) {
366
383
  return toDeveloperKey(privateKey);
367
384
  }
368
385
  function toDeveloperKey(privateKey) {
369
- const raw = Buffer.from(createPublicKey(privateKey).export({ format: "jwk" }).x, "base64url");
386
+ const publicKeyJwk = createPublicKey(privateKey).export({ format: "jwk" });
387
+ const rawPublicKey = Buffer.from(publicKeyJwk.x, "base64url");
370
388
  return {
371
- publicKey: raw.toString("base64"),
372
- keyId: raw.subarray(0, 8).toString("hex"),
389
+ publicKey: rawPublicKey.toString("base64"),
390
+ keyId: rawPublicKey.subarray(0, 8).toString("hex"),
373
391
  sign: (bytes) => sign(null, bytes, privateKey).toString("base64")
374
392
  };
375
393
  }
376
394
  //#endregion
377
395
  //#region src/device/server.ts
378
396
  const DEVICE_SCHEME = "dotdev";
397
+ const FILES_PATH = "/dev/files/";
379
398
  const WAIT_MS = 25e3;
380
399
  const MAX_LOG_BYTES = 1048576;
381
400
  const LINK_LIFETIME_SECONDS = 43200;
@@ -390,130 +409,170 @@ function signedLinkMessage(server, secret, expires, keyId) {
390
409
  ].join("\n");
391
410
  }
392
411
  function deviceToken(secret) {
393
- return createHmac("sha256", Buffer.from(secret, "base64url")).update("dotdev-token", "utf8").digest("base64url");
412
+ return hmacWithSecret(secret, "dotdev-token");
394
413
  }
395
414
  function buildMac(secret, body) {
396
- return createHmac("sha256", Buffer.from(secret, "base64url")).update(`dotdev-build\n${body}`, "utf8").digest("base64url");
415
+ return hmacWithSecret(secret, `dotdev-build\n${body}`);
397
416
  }
398
417
  async function startDeviceServer({ address = localNetworkAddress(), port = 0, onLog = () => {}, onConnect = () => {}, developerKey } = {}) {
399
418
  if (address === void 0) throw new Error("This computer has no local network address, so devices can't reach it. Connect to a network and try again");
400
- const secret = randomBytes(16).toString("base64url");
401
- const token = deviceToken(secret);
402
- let signature = null;
403
- let build = { files: /* @__PURE__ */ new Map() };
404
- let generation = 0;
405
- let hotSince = 0;
406
- let error = null;
407
- const waiting = /* @__PURE__ */ new Set();
408
- const connected = /* @__PURE__ */ new Set();
409
- const announce = () => {
410
- generation += 1;
411
- for (const wake of waiting) wake();
412
- };
413
- const server = createServer((request, response) => {
419
+ const deviceServer = new LocalDeviceServer(onLog, onConnect);
420
+ await deviceServer.listen(port, address, developerKey);
421
+ return deviceServer;
422
+ }
423
+ var LocalDeviceServer = class {
424
+ #onLog;
425
+ #onConnect;
426
+ #secret = randomBytes(16).toString("base64url");
427
+ #token = deviceToken(this.#secret);
428
+ #server;
429
+ #waiting = /* @__PURE__ */ new Set();
430
+ #connected = /* @__PURE__ */ new Set();
431
+ #link = "";
432
+ #signature = null;
433
+ #files = /* @__PURE__ */ new Map();
434
+ #generation = 0;
435
+ #hotSince = 0;
436
+ #error = null;
437
+ constructor(onLog, onConnect) {
438
+ this.#onLog = onLog;
439
+ this.#onConnect = onConnect;
440
+ this.#server = createServer((request, response) => {
441
+ this.#handle(request, response);
442
+ });
443
+ }
444
+ get link() {
445
+ return this.#link;
446
+ }
447
+ async listen(port, address, developerKey) {
448
+ await new Promise((resolve, reject) => {
449
+ this.#server.once("error", reject);
450
+ this.#server.listen(port, address, () => resolve());
451
+ });
452
+ const { port: listeningPort } = this.#server.address();
453
+ const origin = `http://${address}:${listeningPort}`;
454
+ if (developerKey) {
455
+ const expires = Math.floor(Date.now() / 1e3) + LINK_LIFETIME_SECONDS;
456
+ const keyId = developerKey.keyId;
457
+ const message = signedLinkMessage(origin, this.#secret, expires, keyId);
458
+ const sig = developerKey.sign(Buffer.from(message, "utf8"));
459
+ this.#signature = {
460
+ expires,
461
+ keyId,
462
+ sig
463
+ };
464
+ }
465
+ const server = encodeURIComponent(origin);
466
+ this.#link = `${DEVICE_SCHEME}://connect?server=${server}&secret=${this.#secret}`;
467
+ }
468
+ async publish(dir, { hot = false } = {}) {
469
+ this.#files = await readBuild(dir);
470
+ this.#error = null;
471
+ this.#announce();
472
+ if (!hot) this.#hotSince = this.#generation;
473
+ }
474
+ fail(message) {
475
+ this.#error = message;
476
+ this.#announce();
477
+ }
478
+ close() {
479
+ for (const wake of this.#waiting) wake();
480
+ return new Promise((resolve) => {
481
+ this.#server.close(() => resolve());
482
+ this.#server.closeAllConnections();
483
+ });
484
+ }
485
+ #announce() {
486
+ this.#generation += 1;
487
+ for (const wake of this.#waiting) wake();
488
+ }
489
+ #handle(request, response) {
414
490
  const from = request.socket.remoteAddress ?? "a device";
415
491
  if (request.method === "GET" && request.url === "/dev/link") {
416
- if (signature) sendJson(response, signature);
417
- else response.writeHead(404).end();
492
+ this.#sendSignature(response);
418
493
  return;
419
494
  }
420
- if (!isAuthorized(request, token)) {
495
+ if (!isAuthorized(request, this.#token)) {
421
496
  response.writeHead(401).end();
422
497
  return;
423
498
  }
424
499
  const url = new URL(request.url ?? "/", "http://device");
425
500
  const path = url.pathname;
426
- if (request.method === "GET" && path === "/dev/build") {
427
- if (!connected.has(from)) {
428
- connected.add(from);
429
- onConnect(from);
430
- }
431
- const files = Object.fromEntries([...build.files].map(([name, file]) => [name, file.hash]));
432
- const body = JSON.stringify({
433
- generation,
434
- hotSince,
435
- files,
436
- error
437
- });
438
- response.writeHead(200, {
439
- "content-type": "application/json",
440
- [BUILD_MAC_HEADER]: buildMac(secret, body)
441
- }).end(body);
442
- } else if (request.method === "GET" && path.startsWith("/dev/files/")) {
443
- if (url.searchParams.get("generation") !== String(generation)) {
444
- response.writeHead(409).end();
445
- return;
446
- }
447
- const name = path.slice(11).split("/").map(decodeURIComponent).join("/");
448
- const file = build.files.get(name);
449
- if (!file) {
450
- response.writeHead(404).end();
451
- return;
452
- }
453
- response.writeHead(200, { "content-type": "application/octet-stream" }).end(file.bytes);
454
- } else if (request.method === "GET" && path === "/dev/changes") waitForChange(response, Number(url.searchParams.get("after")));
455
- else if (request.method === "POST" && path === "/dev/log") readText(request).then((text) => {
456
- for (const line of text.split("\n")) onLog(line, from);
457
- response.writeHead(204).end();
458
- }).catch(() => response.writeHead(413).end());
501
+ if (request.method === "GET" && path === "/dev/build") this.#sendBuild(response, from);
502
+ else if (request.method === "GET" && path.startsWith(FILES_PATH)) this.#sendFile(response, path, url.searchParams.get("generation"));
503
+ else if (request.method === "GET" && path === "/dev/changes") this.#waitForChange(response, Number(url.searchParams.get("after")));
504
+ else if (request.method === "POST" && path === "/dev/log") this.#receiveLog(request, response, from);
459
505
  else response.writeHead(404).end();
460
- });
461
- const waitForChange = (response, after) => {
462
- if (generation > after) {
463
- sendJson(response, { generation });
506
+ }
507
+ #sendSignature(response) {
508
+ if (this.#signature) sendJson(response, this.#signature);
509
+ else response.writeHead(404).end();
510
+ }
511
+ #sendBuild(response, from) {
512
+ if (!this.#connected.has(from)) {
513
+ this.#connected.add(from);
514
+ this.#onConnect(from);
515
+ }
516
+ const hashes = {};
517
+ for (const [name, file] of this.#files) hashes[name] = file.hash;
518
+ const body = JSON.stringify({
519
+ generation: this.#generation,
520
+ hotSince: this.#hotSince,
521
+ files: hashes,
522
+ error: this.#error
523
+ });
524
+ response.writeHead(200, {
525
+ "content-type": "application/json",
526
+ [BUILD_MAC_HEADER]: buildMac(this.#secret, body)
527
+ }).end(body);
528
+ }
529
+ #sendFile(response, path, generation) {
530
+ if (generation !== String(this.#generation)) {
531
+ response.writeHead(409).end();
532
+ return;
533
+ }
534
+ const fileName = buildFileName(path);
535
+ if (fileName === void 0) {
536
+ response.writeHead(400).end();
537
+ return;
538
+ }
539
+ const file = this.#files.get(fileName);
540
+ if (!file) {
541
+ response.writeHead(404).end();
542
+ return;
543
+ }
544
+ response.writeHead(200, { "content-type": "application/octet-stream" }).end(file.bytes);
545
+ }
546
+ #waitForChange(response, after) {
547
+ if (this.#generation > after) {
548
+ sendJson(response, { generation: this.#generation });
464
549
  return;
465
550
  }
466
551
  const wake = () => {
467
552
  clearTimeout(timer);
468
- waiting.delete(wake);
469
- sendJson(response, { generation });
553
+ this.#waiting.delete(wake);
554
+ sendJson(response, { generation: this.#generation });
470
555
  };
471
556
  const timer = setTimeout(() => {
472
- waiting.delete(wake);
557
+ this.#waiting.delete(wake);
473
558
  response.writeHead(204).end();
474
559
  }, WAIT_MS);
475
- waiting.add(wake);
560
+ this.#waiting.add(wake);
476
561
  response.on("close", () => {
477
562
  clearTimeout(timer);
478
- waiting.delete(wake);
563
+ this.#waiting.delete(wake);
479
564
  });
480
- };
481
- await new Promise((resolve, reject) => {
482
- server.once("error", reject);
483
- server.listen(port, address, () => resolve());
484
- });
485
- const { port: listening } = server.address();
486
- const origin = `http://${address}:${listening}`;
487
- if (developerKey) {
488
- const expires = Math.floor(Date.now() / 1e3) + LINK_LIFETIME_SECONDS;
489
- const keyId = developerKey.keyId;
490
- const message = signedLinkMessage(origin, secret, expires, keyId);
491
- signature = {
492
- expires,
493
- keyId,
494
- sig: developerKey.sign(Buffer.from(message, "utf8"))
495
- };
496
565
  }
497
- return {
498
- link: `${DEVICE_SCHEME}://connect?server=${encodeURIComponent(origin)}&secret=${secret}`,
499
- async publish(dir, { hot = false } = {}) {
500
- build = await readBuild(dir);
501
- error = null;
502
- announce();
503
- if (!hot) hotSince = generation;
504
- },
505
- fail(message) {
506
- error = message;
507
- announce();
508
- },
509
- close() {
510
- for (const wake of waiting) wake();
511
- return new Promise((resolve) => {
512
- server.close(() => resolve());
513
- server.closeAllConnections();
514
- });
515
- }
516
- };
566
+ #receiveLog(request, response, from) {
567
+ readLogText(request).then((text) => {
568
+ for (const line of text.split("\n")) this.#onLog(line, from);
569
+ response.writeHead(204).end();
570
+ }).catch(() => response.writeHead(413).end());
571
+ }
572
+ };
573
+ function hmacWithSecret(secret, text) {
574
+ const key = Buffer.from(secret, "base64url");
575
+ return createHmac("sha256", key).update(text, "utf8").digest("base64url");
517
576
  }
518
577
  function localNetworkAddress() {
519
578
  for (const addresses of Object.values(networkInterfaces())) for (const candidate of addresses ?? []) if (candidate.family === "IPv4" && !candidate.internal) return candidate.address;
@@ -529,12 +588,21 @@ async function readBuild(dir) {
529
588
  const path = join(entry.parentPath, entry.name);
530
589
  const bytes = await readFile(path);
531
590
  const hash = createHash("sha256").update(bytes).digest("base64url");
532
- files.set(relative(dir, path).split(sep).join("/"), {
591
+ const name = relative(dir, path).split(sep).join("/");
592
+ files.set(name, {
533
593
  bytes,
534
594
  hash
535
595
  });
536
596
  }
537
- return { files };
597
+ return files;
598
+ }
599
+ function buildFileName(path) {
600
+ const encodedParts = path.slice(11).split("/");
601
+ try {
602
+ return encodedParts.map((part) => decodeURIComponent(part)).join("/");
603
+ } catch {
604
+ return;
605
+ }
538
606
  }
539
607
  function isAuthorized(request, token) {
540
608
  const given = Buffer.from(request.headers.authorization ?? "");
@@ -544,7 +612,7 @@ function isAuthorized(request, token) {
544
612
  function sendJson(response, body) {
545
613
  response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(body));
546
614
  }
547
- async function readText(request) {
615
+ async function readLogText(request) {
548
616
  const chunks = [];
549
617
  let size = 0;
550
618
  for await (const chunk of request) {
@@ -570,64 +638,40 @@ function startDotSimulator(project, outDir, port) {
570
638
  const CHANGE_DEBOUNCE_MS = 50;
571
639
  async function dev({ port, device, ...options }) {
572
640
  const project = await loadProject(options.root);
573
- const outDir = resolve(project.root, options.outDir ?? "dist");
641
+ const outDir = resolveOutDir(project.root, options.outDir);
574
642
  const buildOptions = {
575
643
  mode: "development",
576
644
  ...options,
577
645
  outDir
578
646
  };
579
- const buildApp = async () => {
580
- const started = performance.now();
581
- try {
582
- printWarnings(await build(buildOptions));
583
- printBuildTime(started);
584
- return null;
585
- } catch (error) {
586
- const message = describeError(error).replaceAll(project.root + sep, "");
587
- console.error(`error: ${message}`);
588
- return message;
589
- }
590
- };
591
- const firstError = await buildApp();
647
+ const firstError = await buildDot(buildOptions, project.root);
592
648
  const simulator = await startDotSimulator(project, outDir, port);
593
649
  if (firstError) simulator.error(firstError);
594
- console.log(`\n Simulator running at: ${simulator.url}\n`);
595
- let devices;
596
- if (device) {
597
- const developerKey = await loadDeveloperKey();
598
- devices = await startDeviceServer({
599
- developerKey,
600
- onConnect: (from) => console.log(`A device connected from ${from}`),
601
- onLog: (line) => console.log(`device ${line}`)
602
- });
603
- if (firstError) devices.fail(firstError);
604
- else await devices.publish(outDir);
605
- printDeviceLink(devices.link);
606
- if (developerKey) console.log(` Signed with developer key ${developerKey.keyId}\n`);
607
- else console.log(`${missingDeveloperKeyHint()}\n`);
608
- }
609
- const changed = /* @__PURE__ */ new Set();
650
+ printSimulatorUrl(simulator.url);
651
+ let deviceServer;
652
+ if (device) deviceServer = await startDevices(outDir, firstError);
653
+ const changedFiles = /* @__PURE__ */ new Set();
610
654
  const pagesDir = join(project.root, PAGES_DIR) + sep;
611
655
  const rebuilds = new RebuildQueue(async () => {
612
- const building = [...changed];
613
- const error = await buildApp();
656
+ const filesInThisBuild = [...changedFiles];
657
+ const error = await buildDot(buildOptions, project.root);
614
658
  if (error) {
615
659
  simulator.error(error);
616
- devices?.fail(error);
660
+ deviceServer?.fail(error);
617
661
  return;
618
662
  }
619
- for (const path of building) changed.delete(path);
620
- const hot = building.every((path) => path.startsWith(pagesDir));
663
+ for (const path of filesInThisBuild) changedFiles.delete(path);
664
+ const hot = filesInThisBuild.every((path) => path.startsWith(pagesDir));
621
665
  if (hot) simulator.hotReload();
622
666
  else simulator.reload();
623
- await devices?.publish(outDir, { hot });
667
+ await deviceServer?.publish(outDir, { hot });
624
668
  });
625
669
  let debounceTimer;
626
670
  const watcher = watch(project.root, { recursive: true }, (_event, file) => {
627
671
  if (!file || isIgnored(file)) return;
628
672
  const path = resolve(project.root, file);
629
673
  if (path === outDir || path.startsWith(outDir + sep)) return;
630
- changed.add(path);
674
+ changedFiles.add(path);
631
675
  clearTimeout(debounceTimer);
632
676
  debounceTimer = setTimeout(() => {
633
677
  console.log(`Changed ${relative(project.root, path)}`);
@@ -637,12 +681,39 @@ async function dev({ port, device, ...options }) {
637
681
  return async () => {
638
682
  clearTimeout(debounceTimer);
639
683
  watcher.close();
640
- await Promise.all([simulator.close(), devices?.close()]);
684
+ await Promise.all([simulator.close(), deviceServer?.close()]);
641
685
  };
642
686
  }
687
+ async function buildDot(buildOptions, root) {
688
+ const startedAt = performance.now();
689
+ try {
690
+ printWarnings(await build(buildOptions));
691
+ printBuildTime(startedAt);
692
+ return null;
693
+ } catch (error) {
694
+ const message = describeError(error).replaceAll(root + sep, "");
695
+ console.error(`error: ${message}`);
696
+ return message;
697
+ }
698
+ }
699
+ async function startDevices(outDir, firstError) {
700
+ const developerKey = await loadDeveloperKey();
701
+ const deviceServer = await startDeviceServer({
702
+ developerKey,
703
+ onConnect: (from) => console.log(`A device connected from ${from}`),
704
+ onLog: (line) => console.log(`device ${line}`)
705
+ });
706
+ if (firstError) deviceServer.fail(firstError);
707
+ else await deviceServer.publish(outDir);
708
+ printDeviceLink(deviceServer.link);
709
+ if (developerKey) console.log(` Signed with developer key ${developerKey.keyId}\n`);
710
+ else console.log(`${missingDeveloperKeyHint()}\n`);
711
+ return deviceServer;
712
+ }
643
713
  function printDeviceLink(link) {
644
714
  console.log(" To open it in Dash, turn on Enable Dot Debugging in Developer Settings.\n Then tap Live Test on the Dots tab and scan this code.\n");
645
- console.log(renderUnicodeCompact(link, { border: 1 }).replace(/^/gm, " "));
715
+ const qrCode = renderUnicodeCompact(link, { border: 1 });
716
+ console.log(qrCode.replace(/^/gm, " "));
646
717
  console.log(`\n ${link}\n`);
647
718
  }
648
719
  var RebuildQueue = class {
@@ -680,16 +751,16 @@ function isIgnored(file) {
680
751
  //#region src/preview.ts
681
752
  async function preview({ port, ...options }) {
682
753
  const project = await loadProject(options.root);
683
- const outDir = resolve(project.root, options.outDir ?? "dist");
684
- const started = performance.now();
754
+ const outDir = resolveOutDir(project.root, options.outDir);
755
+ const startedAt = performance.now();
685
756
  printWarnings(await build({
686
757
  ...options,
687
758
  outDir,
688
759
  mode: "production"
689
760
  }));
690
- printBuildTime(started);
761
+ printBuildTime(startedAt);
691
762
  const simulator = await startDotSimulator(project, outDir, port);
692
- console.log(`\n Simulator running at: ${simulator.url}\n`);
763
+ printSimulatorUrl(simulator.url);
693
764
  return simulator;
694
765
  }
695
766
  //#endregion
@@ -701,29 +772,33 @@ const TERMINAL = {
701
772
  const ERASE_BELOW = "\x1B[0J";
702
773
  const HIDE_CURSOR = "\x1B[?25l";
703
774
  const SHOW_CURSOR = "\x1B[?25h";
704
- const cursorUp = (lines) => `\x1b[${lines}A`;
705
775
  async function select(question, choices, fallback, { input, output } = TERMINAL) {
706
776
  if (!input.isTTY) return fallback;
707
- let current = Math.max(0, choices.findIndex((choice) => choice.value === fallback));
708
- const width = choices.some((choice) => choice.hint) ? Math.max(...choices.map((choice) => choice.value.length)) : 0;
709
- const row = (choice, index) => {
710
- const name = choice.value.padEnd(width);
777
+ let selectedIndex = Math.max(0, choices.findIndex((choice) => choice.value === fallback));
778
+ let valueWidth = 0;
779
+ if (choices.some((choice) => choice.hint)) valueWidth = Math.max(...choices.map((choice) => choice.value.length));
780
+ const formatChoice = (choice, index) => {
781
+ const value = choice.value.padEnd(valueWidth);
711
782
  const hint = choice.hint ? styleText("dim", ` ${choice.hint}`) : "";
712
783
  const defaultMark = choice.value === fallback ? styleText("green", " (default)") : "";
713
- if (index === current) return `${styleText("cyan", "❯")} ${styleText(["cyan", "bold"], name)}${hint}${defaultMark}`;
714
- return ` ${name}${hint}${defaultMark}`;
784
+ if (index === selectedIndex) return `${styleText("cyan", "❯")} ${styleText(["cyan", "bold"], value)}${hint}${defaultMark}`;
785
+ return ` ${value}${hint}${defaultMark}`;
715
786
  };
716
787
  let drawnLines = 0;
717
788
  const draw = (lines) => {
718
- output.write(`${drawnLines ? cursorUp(drawnLines) : ""}${ERASE_BELOW}${lines.join("\n")}\n`);
789
+ const moveUp = drawnLines > 0 ? cursorUp(drawnLines) : "";
790
+ output.write(`${moveUp}${ERASE_BELOW}${lines.join("\n")}\n`);
719
791
  drawnLines = lines.length;
720
792
  };
721
- const drawList = () => draw([`${styleText("bold", question)} ${styleText("dim", "↑/↓ to move, Enter to choose")}`, ...choices.map(row)]);
793
+ const drawQuestionAndChoices = () => {
794
+ const keysHint = styleText("dim", "↑/↓ to move, Enter to choose");
795
+ draw([`${styleText("bold", question)} ${keysHint}`, ...choices.map(formatChoice)]);
796
+ };
722
797
  emitKeypressEvents(input);
723
798
  input.setRawMode(true);
724
799
  input.resume();
725
800
  output.write(HIDE_CURSOR);
726
- drawList();
801
+ drawQuestionAndChoices();
727
802
  return new Promise((resolve) => {
728
803
  const onKeypress = (_text, key) => {
729
804
  if (key.ctrl && key.name === "c") {
@@ -731,13 +806,13 @@ async function select(question, choices, fallback, { input, output } = TERMINAL)
731
806
  process.exit(130);
732
807
  }
733
808
  if (key.name === "up" || key.name === "k") {
734
- current = (current - 1 + choices.length) % choices.length;
735
- drawList();
809
+ selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
810
+ drawQuestionAndChoices();
736
811
  } else if (key.name === "down" || key.name === "j") {
737
- current = (current + 1) % choices.length;
738
- drawList();
812
+ selectedIndex = (selectedIndex + 1) % choices.length;
813
+ drawQuestionAndChoices();
739
814
  } else if (key.name === "return" || key.name === "enter") {
740
- const { value } = choices[current];
815
+ const { value } = choices[selectedIndex];
741
816
  input.off("keypress", onKeypress);
742
817
  input.setRawMode(false);
743
818
  input.pause();
@@ -755,14 +830,14 @@ async function confirm(question, fallback, terminal = TERMINAL) {
755
830
  }
756
831
  async function ask(question, fallback, hint, { input, output } = TERMINAL) {
757
832
  if (!input.isTTY) return fallback;
758
- const guide = hint ? styleText("dim", `, ${hint}`) : "";
833
+ const hintText = hint ? styleText("dim", `, ${hint}`) : "";
759
834
  const defaultMark = fallback ? styleText("dim", ` (default: ${fallback})`) : "";
760
835
  const readline = createInterface({
761
836
  input,
762
837
  output
763
838
  });
764
839
  try {
765
- return (await readline.question(`${styleText("bold", question)}${guide}${defaultMark}: `)).trim() || fallback;
840
+ return (await readline.question(`${styleText("bold", question)}${hintText}${defaultMark}: `)).trim() || fallback;
766
841
  } catch (error) {
767
842
  if (error.name === "AbortError") return;
768
843
  throw error;
@@ -770,6 +845,9 @@ async function ask(question, fallback, hint, { input, output } = TERMINAL) {
770
845
  readline.close();
771
846
  }
772
847
  }
848
+ function cursorUp(lines) {
849
+ return `\x1b[${lines}A`;
850
+ }
773
851
  //#endregion
774
852
  //#region src/cli.ts
775
853
  const USAGE = `Usage:
@@ -822,57 +900,85 @@ async function main(argv) {
822
900
  }
823
901
  });
824
902
  const [command, target, pagePath] = positionals;
825
- if (command === "lsp" && !values.help) {
826
- const { serveStdio } = await import("./server-uIyYmYnp.js");
827
- serveStdio();
828
- return new Promise(() => {});
829
- }
830
- if (command === "keys" && !values.help) return keys(target);
831
- if (command === "create" && target === "page" && !values.help) return addPage(pagePath);
832
- if (command === "create" && !values.help) return createApp(target, values.name, values.package, values.template, values.pm);
833
- if (values.help || command !== "build" && command !== "dev" && command !== "preview") {
903
+ if (values.help) {
834
904
  console.log(USAGE);
835
- return values.help ? 0 : 1;
905
+ return 0;
836
906
  }
837
- const port = values.port === void 0 ? void 0 : Number(values.port);
838
- if (port !== void 0 && !Number.isInteger(port)) {
839
- console.error(`--port must be a whole number, not "${values.port}"`);
840
- return 1;
907
+ switch (command) {
908
+ case "lsp": return serveLanguageServer();
909
+ case "keys": return showKeys(target);
910
+ case "create":
911
+ if (target === "page") return addPage(pagePath);
912
+ return createDot(target, {
913
+ name: values.name,
914
+ dotPackage: values.package,
915
+ template: values.template,
916
+ packageManager: values.pm
917
+ });
918
+ case "build":
919
+ case "dev":
920
+ case "preview": break;
921
+ default:
922
+ console.log(USAGE);
923
+ return 1;
924
+ }
925
+ let port;
926
+ if (values.port !== void 0) {
927
+ port = Number(values.port);
928
+ if (!Number.isInteger(port)) {
929
+ console.error(`--port must be a whole number, not "${values.port}"`);
930
+ return 1;
931
+ }
841
932
  }
842
933
  const options = { root: target ?? "." };
843
934
  if (values["out-dir"] !== void 0) options.outDir = values["out-dir"];
844
935
  if (values.conditions !== void 0) options.conditions = values.conditions.split(",");
845
- if (command === "dev") {
846
- const stop = await dev({
847
- ...options,
848
- port,
849
- device: values.device
850
- });
851
- await new Promise((resolve) => process.once("SIGINT", resolve));
852
- await stop();
853
- return 0;
854
- }
855
- if (command === "preview") {
856
- if (values.device) {
857
- console.error("--device only works with dotctl dev");
858
- return 1;
859
- }
860
- const simulator = await preview({
861
- ...options,
862
- port
863
- });
864
- await new Promise((resolve) => process.once("SIGINT", resolve));
865
- await simulator.close();
866
- return 0;
936
+ if (command === "dev") return runDev(options, port, values.device);
937
+ if (command === "preview") return runPreview(options, port, values.device);
938
+ return runBuild(options);
939
+ }
940
+ async function serveLanguageServer() {
941
+ const { serveStdio } = await import("./server-B9-F93mS.js");
942
+ serveStdio();
943
+ return new Promise(() => {});
944
+ }
945
+ async function runDev(options, port, device) {
946
+ const stop = await dev({
947
+ ...options,
948
+ port,
949
+ device
950
+ });
951
+ await waitForCtrlC();
952
+ await stop();
953
+ return 0;
954
+ }
955
+ async function runPreview(options, port, device) {
956
+ if (device) {
957
+ console.error("--device only works with dotctl dev");
958
+ return 1;
867
959
  }
868
- const started = performance.now();
960
+ const simulator = await preview({
961
+ ...options,
962
+ port
963
+ });
964
+ await waitForCtrlC();
965
+ await simulator.close();
966
+ return 0;
967
+ }
968
+ async function runBuild(options) {
969
+ const startedAt = performance.now();
869
970
  const result = await build(options);
870
- for (const file of result.files.filter((file) => !file.endsWith(".map"))) console.log(` ${relative(process.cwd(), file)}`);
971
+ printFiles(result.files.filter((file) => !file.endsWith(".map")));
871
972
  printWarnings(result);
872
- printBuildTime(started);
973
+ printBuildTime(startedAt);
873
974
  return 0;
874
975
  }
875
- async function keys(action) {
976
+ function waitForCtrlC() {
977
+ return new Promise((resolve) => {
978
+ process.once("SIGINT", () => resolve());
979
+ });
980
+ }
981
+ async function showKeys(action) {
876
982
  if (action !== "show") {
877
983
  console.error("Usage: dotctl keys show");
878
984
  return 1;
@@ -886,40 +992,51 @@ async function keys(action) {
886
992
  console.log(`\n Public key: ${key.publicKey}\n Key ID: ${key.keyId}\n`);
887
993
  return 0;
888
994
  }
889
- async function addPage(path) {
890
- if (path === void 0) {
995
+ async function addPage(typedPath) {
996
+ if (typedPath === void 0) {
891
997
  console.error("Give the page a path, such as: dotctl create page cart/checkout");
892
998
  return 1;
893
999
  }
894
- if (path.startsWith("/")) path = path.substring(1);
895
- if (path.endsWith("/")) path = path.substring(0, path.length - 1);
1000
+ const path = trimSlashes(typedPath);
896
1001
  const { files, listing } = await createPage(path, process.cwd());
897
- for (const file of files) console.log(` ${relative(process.cwd(), file)}`);
1002
+ printFiles(files);
898
1003
  if (listing === "added") console.log(`\nListed '${path}' in dot.ts.`);
899
1004
  else if (listing === "manual") console.log(`\nAdd it to the pages in dot.ts so it gets built: defineDot({ pages: [..., '${path}'] })`);
900
1005
  return 0;
901
1006
  }
902
- async function createApp(dir, name, dotPackageFlag, templateFlag, packageManagerFlag) {
1007
+ function trimSlashes(path) {
1008
+ let trimmed = path;
1009
+ if (trimmed.startsWith("/")) trimmed = trimmed.substring(1);
1010
+ if (trimmed.endsWith("/")) trimmed = trimmed.substring(0, trimmed.length - 1);
1011
+ return trimmed;
1012
+ }
1013
+ async function createDot(dir, flags) {
1014
+ const templateFlag = flags.template;
903
1015
  if (templateFlag !== void 0 && !isTemplate(templateFlag)) {
904
1016
  console.error(`--template must be one of ${TEMPLATES.join(", ")}`);
905
1017
  return 1;
906
1018
  }
1019
+ const packageManagerFlag = flags.packageManager;
907
1020
  if (packageManagerFlag !== void 0 && !isPackageManager(packageManagerFlag)) {
908
1021
  console.error(`--pm must be one of ${PACKAGE_MANAGERS.join(", ")}`);
909
1022
  return 1;
910
1023
  }
911
- name ??= await ask("Dot name", dir ? toDisplayName(dir) : "Dot");
1024
+ let name = flags.name;
1025
+ if (name === void 0) name = await ask("Dot name", dir ? toDisplayName(dir) : "Dot");
912
1026
  if (!name) {
913
1027
  console.error("The dot needs a name. Pass one with: dotctl create [dir] --name \"My Dot\"");
914
1028
  return 1;
915
1029
  }
1030
+ const dotPackageFlag = flags.dotPackage;
916
1031
  if (dotPackageFlag !== void 0 && !isDotPackage(dotPackageFlag)) {
917
1032
  console.error(`--package "${dotPackageFlag}" isn't a package name. ${DOT_PACKAGE_RULE}`);
918
1033
  return 1;
919
1034
  }
920
- const dotPackage = dotPackageFlag ?? await askDotPackage(suggestedDotPackage(toPackageName(name)));
1035
+ let dotPackage = dotPackageFlag;
1036
+ if (dotPackage === void 0) dotPackage = await askDotPackage(suggestedDotPackage(toPackageName(name)));
921
1037
  if (!dotPackage) return 1;
922
- const template = templateFlag ?? await chooseTemplate();
1038
+ let template = templateFlag;
1039
+ if (template === void 0) template = await chooseTemplate();
923
1040
  const { root, workspace } = await create({
924
1041
  name,
925
1042
  dir: dir ?? toPackageName(name),
@@ -929,17 +1046,19 @@ async function createApp(dir, name, dotPackageFlag, templateFlag, packageManager
929
1046
  const path = relative(process.cwd(), root) || ".";
930
1047
  console.log(`\nCreated ${name} in ${path}`);
931
1048
  if (workspace) console.log("It is inside the Dot repository, so it uses the workspace packages.");
932
- let packageManager = workspace ? "pnpm" : packageManagerFlag ?? launchedBy();
1049
+ let packageManager;
1050
+ if (workspace) packageManager = "pnpm";
1051
+ else packageManager = packageManagerFlag ?? launchedBy();
933
1052
  console.log();
934
1053
  if (await confirm("Install and start it now?", true)) {
935
1054
  if (!workspace && !packageManagerFlag) packageManager = await choosePackageManager(packageManager);
936
- if (await run(packageManager, ["install"], root) === 0) return run(packageManager, ["run", "dev"], root);
1055
+ if (await runCommand(packageManager, ["install"], root) === 0) return runCommand(packageManager, ["run", "dev"], root);
937
1056
  console.error(`\n${packageManager} install failed. Fix the problem above, then run the steps below.`);
938
1057
  }
939
1058
  console.log(`\nNext:\n\n cd ${path}\n ${packageManager} install\n ${packageManager} run dev\n`);
940
1059
  return 0;
941
1060
  }
942
- function run(command, args, cwd) {
1061
+ function runCommand(command, args, cwd) {
943
1062
  return new Promise((resolvePromise, reject) => {
944
1063
  const child = spawn(command, args, {
945
1064
  cwd,
@@ -967,10 +1086,11 @@ function chooseTemplate() {
967
1086
  async function choosePackageManager(suggested) {
968
1087
  const installed = installedManagers();
969
1088
  if (installed.length < 2) return installed[0] ?? suggested;
970
- const fallback = installed.includes(suggested) ? suggested : installed[0];
1089
+ let fallback = installed[0];
1090
+ if (installed.includes(suggested)) fallback = suggested;
971
1091
  return select("Package manager?", installed.map((manager) => ({ value: manager })), fallback);
972
1092
  }
973
- main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
1093
+ main(process.argv.slice(2)).then((exitCode) => process.exit(exitCode), (error) => {
974
1094
  console.error(`error: ${describeError(error)}`);
975
1095
  process.exit(1);
976
1096
  });