@monimesl/dot-cli 0.1.0-alpha.9 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +72 -59
  2. package/dist/cli.js +539 -341
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{server-BfPAnsSf.js → server-DMnWDdJ6.js} +570 -478
  5. package/dist/server-DMnWDdJ6.js.map +1 -0
  6. package/dist/templates/showcase/dot.ts +53 -16
  7. package/dist/templates/showcase/pages/basics/bars/controller.ts +47 -0
  8. package/dist/templates/showcase/pages/basics/bars/state.ts +7 -0
  9. package/dist/templates/showcase/pages/basics/bars/view.dml +36 -0
  10. package/dist/templates/showcase/pages/{text → basics/bindings}/controller.ts +5 -0
  11. package/dist/templates/showcase/pages/basics/bindings/state.ts +14 -0
  12. package/dist/templates/showcase/pages/basics/bindings/view.dml +41 -0
  13. package/dist/templates/showcase/pages/basics/colors/controller.ts +112 -0
  14. package/dist/templates/showcase/pages/basics/colors/palette.ts +52 -0
  15. package/dist/templates/showcase/pages/basics/colors/state.ts +17 -0
  16. package/dist/templates/showcase/pages/basics/colors/view.dml +142 -0
  17. package/dist/templates/showcase/pages/basics/concert/controller.ts +16 -0
  18. package/dist/templates/showcase/pages/basics/concert/state.ts +7 -0
  19. package/dist/templates/showcase/pages/basics/concert/view.dml +20 -0
  20. package/dist/templates/showcase/pages/basics/control-flow/controller.ts +18 -0
  21. package/dist/templates/showcase/pages/basics/control-flow/state.ts +16 -0
  22. package/dist/templates/showcase/pages/basics/control-flow/view.dml +55 -0
  23. package/dist/templates/showcase/pages/basics/events/controller.ts +43 -0
  24. package/dist/templates/showcase/pages/basics/events/state.ts +11 -0
  25. package/dist/templates/showcase/pages/basics/events/view.dml +51 -0
  26. package/dist/templates/showcase/pages/basics/hooks/controller.ts +43 -0
  27. package/dist/templates/showcase/pages/basics/hooks/state.ts +10 -0
  28. package/dist/templates/showcase/pages/basics/hooks/view.dml +37 -0
  29. package/dist/templates/showcase/pages/basics/navigation/controller.ts +41 -0
  30. package/dist/templates/showcase/pages/basics/navigation/state.ts +11 -0
  31. package/dist/templates/showcase/pages/basics/navigation/view.dml +48 -0
  32. package/dist/templates/showcase/pages/basics/pick-genre/controller.ts +18 -0
  33. package/dist/templates/showcase/pages/basics/pick-genre/state.ts +3 -0
  34. package/dist/templates/showcase/pages/basics/pick-genre/view.dml +10 -0
  35. package/dist/templates/showcase/pages/basics/share/concerts.ts +31 -0
  36. package/dist/templates/showcase/pages/basics/share/controller.ts +12 -0
  37. package/dist/templates/showcase/pages/basics/share/state.ts +5 -0
  38. package/dist/templates/showcase/pages/basics/share/view.dml +21 -0
  39. package/dist/templates/showcase/pages/{state → basics/state}/controller.ts +1 -0
  40. package/dist/templates/showcase/pages/basics/state/view.dml +46 -0
  41. package/dist/templates/showcase/pages/{carousel → components/carousel}/controller.ts +2 -1
  42. package/dist/templates/showcase/pages/{carousel → components/carousel}/state.ts +5 -5
  43. package/dist/templates/showcase/pages/{carousel → components/carousel}/view.dml +7 -5
  44. package/dist/templates/showcase/pages/{feedback → components/feedback}/controller.ts +13 -2
  45. package/dist/templates/showcase/pages/components/feedback/state.ts +41 -0
  46. package/dist/templates/showcase/pages/{feedback → components/feedback}/view.dml +38 -9
  47. package/dist/templates/showcase/pages/{forms → components/forms}/controller.ts +23 -19
  48. package/dist/templates/showcase/pages/{forms → components/forms}/state.ts +6 -3
  49. package/dist/templates/showcase/pages/components/forms/view.dml +97 -0
  50. package/dist/templates/showcase/pages/components/icons/controller.ts +15 -0
  51. package/dist/templates/showcase/pages/components/icons/state.ts +75 -0
  52. package/dist/templates/showcase/pages/components/icons/view.dml +36 -0
  53. package/dist/templates/showcase/pages/{images → components/images}/controller.ts +4 -1
  54. package/dist/templates/showcase/pages/components/images/view.dml +83 -0
  55. package/dist/templates/showcase/pages/{layout → components/layout}/controller.ts +3 -2
  56. package/dist/templates/showcase/pages/components/layout/view.dml +143 -0
  57. package/dist/templates/showcase/pages/components/lists/controller.ts +18 -0
  58. package/dist/templates/showcase/pages/{lists → components/lists}/state.ts +43 -30
  59. package/dist/templates/showcase/pages/components/lists/view.dml +88 -0
  60. package/dist/templates/showcase/pages/{payments → components/money}/controller.ts +7 -8
  61. package/dist/templates/showcase/pages/components/money/view.dml +54 -0
  62. package/dist/templates/showcase/pages/components/reading/controller.ts +6 -0
  63. package/dist/templates/showcase/pages/components/reading/state.ts +23 -0
  64. package/dist/templates/showcase/pages/components/reading/view.dml +42 -0
  65. package/dist/templates/showcase/pages/{lists → components/sheets}/controller.ts +10 -11
  66. package/dist/templates/showcase/pages/components/sheets/state.ts +26 -0
  67. package/dist/templates/showcase/pages/components/sheets/view.dml +38 -0
  68. package/dist/templates/showcase/pages/components/styles/controller.ts +6 -0
  69. package/dist/templates/showcase/pages/components/styles/state.ts +3 -0
  70. package/dist/templates/showcase/pages/components/styles/view.dml +101 -0
  71. package/dist/templates/showcase/pages/components/tabs/controller.ts +41 -0
  72. package/dist/templates/showcase/pages/{filters → components/tabs}/state.ts +32 -23
  73. package/dist/templates/showcase/pages/components/tabs/view.dml +57 -0
  74. package/dist/templates/showcase/pages/components/text/controller.ts +10 -0
  75. package/dist/templates/showcase/pages/components/text/state.ts +3 -0
  76. package/dist/templates/showcase/pages/components/text/view.dml +95 -0
  77. package/dist/templates/showcase/pages/dash/account/controller.ts +32 -0
  78. package/dist/templates/showcase/pages/dash/account/state.ts +9 -0
  79. package/dist/templates/showcase/pages/dash/account/view.dml +33 -0
  80. package/dist/templates/showcase/pages/{cache → dash/cache}/controller.ts +9 -4
  81. package/dist/templates/showcase/pages/dash/cache/view.dml +48 -0
  82. package/dist/templates/showcase/pages/dash/calls/controller.ts +67 -0
  83. package/dist/templates/showcase/pages/dash/calls/state.ts +9 -0
  84. package/dist/templates/showcase/pages/dash/calls/view.dml +69 -0
  85. package/dist/templates/showcase/pages/dash/contacts/controller.ts +30 -0
  86. package/dist/templates/showcase/pages/dash/contacts/state.ts +6 -0
  87. package/dist/templates/showcase/pages/dash/contacts/view.dml +33 -0
  88. package/dist/templates/showcase/pages/dash/crypto/controller.ts +38 -0
  89. package/dist/templates/showcase/pages/dash/crypto/state.ts +10 -0
  90. package/dist/templates/showcase/pages/dash/crypto/view.dml +48 -0
  91. package/dist/templates/showcase/pages/{files → dash/files}/controller.ts +7 -2
  92. package/dist/templates/showcase/pages/{files → dash/files}/state.ts +4 -0
  93. package/dist/templates/showcase/pages/dash/files/view.dml +78 -0
  94. package/dist/templates/showcase/pages/dash/media/controller.ts +65 -0
  95. package/dist/templates/showcase/pages/dash/media/state.ts +12 -0
  96. package/dist/templates/showcase/pages/dash/media/view.dml +53 -0
  97. package/dist/templates/showcase/pages/{network → dash/network}/controller.ts +84 -20
  98. package/dist/templates/showcase/pages/{network → dash/network}/state.ts +14 -1
  99. package/dist/templates/showcase/pages/dash/network/view.dml +108 -0
  100. package/dist/templates/showcase/pages/dash/payments/cases.ts +445 -0
  101. package/dist/templates/showcase/pages/dash/payments/controller.ts +149 -0
  102. package/dist/templates/showcase/pages/dash/payments/state.ts +33 -0
  103. package/dist/templates/showcase/pages/dash/payments/view.dml +71 -0
  104. package/dist/templates/showcase/pages/{secrets → dash/secrets}/controller.ts +2 -1
  105. package/dist/templates/showcase/pages/dash/secrets/view.dml +26 -0
  106. package/dist/templates/showcase/pages/dash/system/controller.ts +89 -0
  107. package/dist/templates/showcase/pages/dash/system/state.ts +13 -0
  108. package/dist/templates/showcase/pages/dash/system/view.dml +96 -0
  109. package/dist/templates/showcase/pages/dash/ui/controller.ts +70 -0
  110. package/dist/templates/showcase/pages/dash/ui/state.ts +8 -0
  111. package/dist/templates/showcase/pages/dash/ui/view.dml +31 -0
  112. package/dist/templates/showcase/pages/index/controller.ts +1 -1
  113. package/dist/templates/showcase/pages/index/state.ts +175 -60
  114. package/dist/templates/showcase/pages/index/view.dml +6 -3
  115. package/package.json +6 -6
  116. package/dist/server-BfPAnsSf.js.map +0 -1
  117. package/dist/templates/showcase/pages/cache/view.dml +0 -36
  118. package/dist/templates/showcase/pages/device/controller.ts +0 -80
  119. package/dist/templates/showcase/pages/device/state.ts +0 -7
  120. package/dist/templates/showcase/pages/device/view.dml +0 -49
  121. package/dist/templates/showcase/pages/feedback/state.ts +0 -8
  122. package/dist/templates/showcase/pages/files/view.dml +0 -50
  123. package/dist/templates/showcase/pages/filters/controller.ts +0 -27
  124. package/dist/templates/showcase/pages/filters/view.dml +0 -44
  125. package/dist/templates/showcase/pages/forms/view.dml +0 -49
  126. package/dist/templates/showcase/pages/images/view.dml +0 -53
  127. package/dist/templates/showcase/pages/layout/view.dml +0 -103
  128. package/dist/templates/showcase/pages/lists/view.dml +0 -59
  129. package/dist/templates/showcase/pages/navigation/controller.ts +0 -65
  130. package/dist/templates/showcase/pages/navigation/state.ts +0 -17
  131. package/dist/templates/showcase/pages/navigation/view.dml +0 -34
  132. package/dist/templates/showcase/pages/network/view.dml +0 -67
  133. package/dist/templates/showcase/pages/payments/view.dml +0 -45
  134. package/dist/templates/showcase/pages/secrets/view.dml +0 -24
  135. package/dist/templates/showcase/pages/state/view.dml +0 -33
  136. package/dist/templates/showcase/pages/text/state.ts +0 -8
  137. package/dist/templates/showcase/pages/text/view.dml +0 -58
  138. /package/dist/templates/showcase/pages/{state → basics/state}/state.ts +0 -0
  139. /package/dist/templates/showcase/pages/{images → components/images}/state.ts +0 -0
  140. /package/dist/templates/showcase/pages/{layout → components/layout}/state.ts +0 -0
  141. /package/dist/templates/showcase/pages/{payments → components/money}/state.ts +0 -0
  142. /package/dist/templates/showcase/pages/{cache → dash/cache}/state.ts +0 -0
  143. /package/dist/templates/showcase/pages/{secrets → dash/secrets}/state.ts +0 -0
package/dist/cli.js CHANGED
@@ -2,13 +2,13 @@ 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, 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";
9
9
  import { DmlParseError } from "@monimesl/dot-dml-parser";
10
10
  import { ScriptParseError, parseDotModule } from "@monimesl/dot-script-parser";
11
- import { isPageId } from "@monimesl/dot-core";
11
+ import { DEFAULT_DASH_API_VERSION, isPageId } from "@monimesl/dot-core";
12
12
  import { renderUnicodeCompact } from "uqr";
13
13
  import { createHash, createHmac, createPrivateKey, createPublicKey, randomBytes, sign, timingSafeEqual } from "node:crypto";
14
14
  import { homedir, networkInterfaces } from "node:os";
@@ -16,17 +16,24 @@ import { createServer } from "node:http";
16
16
  import { emitKeypressEvents } from "node:readline";
17
17
  import { createInterface } from "node:readline/promises";
18
18
  //#region src/report.ts
19
- /** A mistake in how a command was used, which its message alone explains. */
20
19
  var CommandError = class extends Error {
21
20
  name = "CommandError";
22
21
  };
23
22
  function describeError(error) {
24
23
  if (error instanceof CommandError || error instanceof CompileError || error instanceof DmlParseError || error instanceof ScriptParseError) return error.message;
25
- if (error.code?.startsWith("ERR_PARSE_ARGS")) {
24
+ if (isArgumentError(error)) {
26
25
  const [firstSentence] = error.message.split(". ");
27
26
  return `${firstSentence}. See dotctl --help.`;
28
27
  }
29
- 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)}`);
30
37
  }
31
38
  function printWarnings({ warnings }) {
32
39
  for (const warning of warnings) console.warn(`warning: ${warning.message}`);
@@ -34,15 +41,38 @@ function printWarnings({ warnings }) {
34
41
  function printBuildTime(startedAt) {
35
42
  console.log(`Built in ${Math.round(performance.now() - startedAt)}ms`);
36
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
+ }
64
+ function isOneOf(values, name) {
65
+ return values.includes(name);
66
+ }
37
67
  //#endregion
38
68
  //#region src/create/template.ts
39
69
  const TEMPLATES = ["starter", "showcase"];
40
70
  const TEMPLATE_HINTS = {
41
71
  starter: "one page to build on",
42
- showcase: "a page for each feature, to read"
72
+ showcase: "a page for each feature, to learn from"
43
73
  };
44
74
  function isTemplate(name) {
45
- return TEMPLATES.includes(name);
75
+ return isOneOf(TEMPLATES, name);
46
76
  }
47
77
  const PAGE_FILES = {
48
78
  "view.dml": "<View>\n <Text>My new page</Text>\n</View>\n",
@@ -60,9 +90,9 @@ const TEMPLATE_FILES = [
60
90
  "statics",
61
91
  ".env"
62
92
  ];
63
- const DOT_JSON_SCHEMA = "./node_modules/@monimesl/dot-runtime/dot.schema.json";
64
- const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
65
- function projectFiles(options) {
93
+ const DOT_JSON_SCHEMA = "https://unpkg.com/@monimesl/dot-runtime/dot.schema.json";
94
+ const UNREGISTERED_DOT_ID = "<monime-Internal-Id>";
95
+ function generatedFiles(options) {
66
96
  return {
67
97
  "package.json": packageJson(options),
68
98
  "tsconfig.json": tsconfig(options),
@@ -73,7 +103,7 @@ function projectFiles(options) {
73
103
  }
74
104
  function packageJson({ packageName, dotVersion, workspace }) {
75
105
  const conditions = workspace ? " --conditions @monimesl/dot-source" : "";
76
- return json({
106
+ return formatJson({
77
107
  name: packageName,
78
108
  private: true,
79
109
  type: "module",
@@ -103,7 +133,7 @@ function tsconfig({ workspace }) {
103
133
  verbatimModuleSyntax: true
104
134
  };
105
135
  if (workspace) compilerOptions.customConditions = ["@monimesl/dot-source"];
106
- return json({
136
+ return formatJson({
107
137
  compilerOptions,
108
138
  include: [
109
139
  "dot.ts",
@@ -113,27 +143,40 @@ function tsconfig({ workspace }) {
113
143
  ]
114
144
  });
115
145
  }
116
- function dotJson({ name, packageName, template }) {
146
+ function dotJson({ name, dotPackage, template }) {
117
147
  const dot = {
148
+ id: UNREGISTERED_DOT_ID,
118
149
  $schema: DOT_JSON_SCHEMA,
119
- id: `com.example.${idWord(packageName)}`,
150
+ package: dotPackage,
120
151
  name,
121
152
  version: "0.1.0"
122
153
  };
123
- if (template === "showcase") dot.network = { hosts: [
124
- "itunes.apple.com",
125
- "*.mzstatic.com",
126
- "httpbin.org"
127
- ] };
128
- return json(dot);
154
+ if (template === "showcase") {
155
+ dot.permissions = {
156
+ account: "Show your name on your concert ticket",
157
+ camera: "Take a photo for your concert ticket, or scan a ticket's QR code",
158
+ photoLibrary: "Choose a photo for your concert ticket",
159
+ files: "Attach your concert ticket",
160
+ contacts: "Choose a friend to send a ticket to",
161
+ payments: "Pay for concert tickets"
162
+ };
163
+ dot.network = { hosts: [
164
+ "itunes.apple.com",
165
+ "*.mzstatic.com",
166
+ "httpbin.org"
167
+ ] };
168
+ }
169
+ dot.dash = { minApiVersion: DEFAULT_DASH_API_VERSION };
170
+ return formatJson(dot);
129
171
  }
130
172
  const TEMPLATE_READMES = {
131
- starter: `It starts with one page, \`index\`: a greeting from \`.env\` and a count to tap. Change it,
132
- and add pages as the dot grows. \`npx dotctl create my-tour --template showcase\` makes a dot with a
133
- page for each feature, to read how each is done.`,
134
- showcase: `It starts as a showcase: \`index\` lists a page for each feature, from layout, text, images and
135
- inputs to state, navigation, and Dash's alerts, network and files. Read a page to see how it's
136
- done, then delete the ones you don't need, from \`pages/\` and from \`dot.ts\`.`
173
+ starter: `It starts with one page, \`index\`. It shows a greeting from \`.env\` and a counter you
174
+ can tap. Change it, and add pages as the dot grows. To see how each feature works, make a showcase
175
+ dot with \`npx dotctl create my-tour --template showcase\`.`,
176
+ showcase: `It starts as a showcase. The \`index\` page links to a page for each feature, in three
177
+ folders: \`pages/basics/\` for how a page works, \`pages/components/\` for what a view can show,
178
+ and \`pages/dash/\` for the phone's features. Read a page to see how it works. Then delete the
179
+ pages you don't need, both from \`pages/\` and from \`dot.ts\`.`
137
180
  };
138
181
  function readme({ name, template }) {
139
182
  return `# ${name}
@@ -146,41 +189,67 @@ npm run dev # run it in the simulator; it reloads on every change
146
189
  npm run build # build it for Dash
147
190
  \`\`\`
148
191
 
149
- Each page is a folder under \`pages/\` with three files: \`view.dml\` (what it shows),
150
- \`state.ts\` (what it starts with) and \`controller.ts\` (how it changes). A page's markup is one
151
- root \`<View>\` with everything else inside it. \`npx dotctl create page <path>\` adds a page, such
152
- as \`cart/checkout\`, with the three files to start from, and lists it in \`dot.ts\`. The first page
153
- there is where the dot starts.
192
+ ${TEMPLATE_READMES[template]}
193
+
194
+ ## Pages
154
195
 
155
- \`dot.json\` describes the dot to Dash. Change its \`id\` from \`com.example.…\` to a reverse domain
156
- name you own before you publish: it can't change afterwards. Its \`network.hosts\` lists every host
157
- the dot sends requests to with \`dash.network\`; requests to any other fail. Your editor completes
158
- the other fields from the schema it points to.
196
+ Each page is a folder under \`pages/\` with three files:
159
197
 
160
- \`.env\` holds public configuration, which the service reads as \`env.GREETING\`. Every variable
161
- there is built into the dot, so anyone who has it can read them: never put a secret there.
162
- \`.env.development\` and \`.env.production\` override it for \`dev\` and \`build\`.
198
+ - \`view.dml\`: what the page shows. It has one root \`<View>\`, with everything else inside it.
199
+ - \`state.ts\`: the data the page starts with.
200
+ - \`controller.ts\`: how the data changes.
163
201
 
164
- ${TEMPLATE_READMES[template]}
202
+ \`npx dotctl create page <path>\` adds a page, such as \`cart/checkout\`, with these three files. It
203
+ also lists the page in \`dot.ts\`. The dot opens on the first page in that list.
204
+
205
+ ## dot.json
206
+
207
+ \`dot.json\` describes the dot to Dash. Its \`package\` is a reverse domain name you own, such as
208
+ \`com.mysalone.thing\`. You can't change it after publishing. To publish, register the dot with
209
+ Monime first, and replace its \`id\`, \`${UNREGISTERED_DOT_ID}\`, with the ID Monime gives you.
210
+ \`network.hosts\` lists every host the dot sends requests to with \`dash.network\`. Requests to any
211
+ other host fail. Your editor can complete the other fields from the schema the file points to.
212
+
213
+ ## Which Dash opens it
214
+
215
+ \`dash.minApiVersion\` in \`dot.json\` is the Dash API version the dot needs, such as \`1.0\`. Dash
216
+ opens the dot when it offers that version, or a newer minor of the same major. Each Dash call's docs
217
+ say which version it came in, after \`@since\`. To use a newer call and still open on older Dash,
218
+ keep \`minApiVersion\` and check \`this.dash.supports('contacts.pickPhone')\` before the call.
219
+ \`dotctl dev\` warns when the dot makes a newer call without checking.
220
+
221
+ ## .env
222
+
223
+ \`.env\` holds public configuration. Code in the dot reads each variable from \`env\`, such as
224
+ \`env.GREETING\`. Every variable is built into the dot, so anyone who has the dot can read it.
225
+ Never put a secret there. \`.env.development\` and \`.env.production\` override it for \`dev\` and
226
+ \`build\`.
165
227
  `;
166
228
  }
167
- function idWord(packageName) {
168
- return packageName.slice(0, 63).replace(/-+$/, "");
229
+ function suggestedDotPackage(packageName) {
230
+ return `com.example.${packageName.slice(0, 63).replace(/-+$/, "")}`;
231
+ }
232
+ function formatJson(value) {
233
+ return `${JSON.stringify(value, null, 2)}\n`;
169
234
  }
170
235
  //#endregion
171
236
  //#region src/create/create.ts
172
- async function create({ name, dir, template = "starter" }) {
237
+ const DOT_PACKAGE_RULE = "Use lowercase words joined by dots, such as com.mysalone.thing.";
238
+ async function create({ name, dir, dotPackage, template = "starter" }) {
173
239
  const root = resolve(dir);
174
- if (existsSync(root) && readdirSync(root).length > 0) throw new CommandError(`${dir} already exists and is not empty`);
240
+ if (existsSync(root) && readdirSync(root).length > 0) throw new CommandError(`${dir} already exists and is not empty. Choose another folder`);
175
241
  const packageName = toPackageName(name);
176
- if (!packageName) throw new CommandError(`"${name}" needs at least one letter or digit`);
242
+ if (!packageName) throw new CommandError(`The name "${name}" needs at least one letter or digit`);
243
+ dotPackage ??= suggestedDotPackage(packageName);
244
+ if (!isDotPackage(dotPackage)) throw new CommandError(`"${dotPackage}" isn't a package name. ${DOT_PACKAGE_RULE}`);
177
245
  const workspace = isInDotWorkspace(root);
178
246
  const source = templateDir(template);
179
247
  await mkdir(root, { recursive: true });
180
248
  for (const path of TEMPLATE_FILES) await cp(join(source, path), join(root, path), { recursive: true });
181
- const generated = projectFiles({
249
+ const generated = generatedFiles({
182
250
  name,
183
251
  packageName,
252
+ dotPackage,
184
253
  dotVersion: workspace ? "workspace:*" : `^${cliVersion()}`,
185
254
  workspace,
186
255
  template
@@ -198,21 +267,16 @@ function toDisplayName(dir) {
198
267
  return basename(resolve(dir)).split(/[-_\s]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
199
268
  }
200
269
  function isInDotWorkspace(dir) {
201
- let current = dirname(dir);
202
- while (true) {
203
- if (existsSync(join(current, "pnpm-workspace.yaml"))) {
204
- const runtimeManifest = join(current, "packages/runtime/package.json");
205
- if (!existsSync(runtimeManifest)) return false;
206
- return readPackageJson(runtimeManifest).name === "@monimesl/dot-runtime";
207
- }
208
- const parent = dirname(current);
209
- if (parent === current) return false;
210
- current = parent;
211
- }
270
+ const workspaceRoot = findFolderWith("pnpm-workspace.yaml", dirname(dir));
271
+ if (workspaceRoot === void 0) return false;
272
+ const runtimeManifest = join(workspaceRoot, "packages/runtime/package.json");
273
+ if (!existsSync(runtimeManifest)) return false;
274
+ return readPackageJson(runtimeManifest).name === "@monimesl/dot-runtime";
212
275
  }
213
276
  function templateDir(template) {
214
277
  const example = join(cliRoot(), "../../examples", template);
215
- return existsSync(example) ? example : join(cliRoot(), "dist/templates", template);
278
+ if (existsSync(example)) return example;
279
+ return join(cliRoot(), "dist/templates", template);
216
280
  }
217
281
  function cliVersion() {
218
282
  return readPackageJson(join(cliRoot(), "package.json")).version;
@@ -224,23 +288,13 @@ function cliRoot() {
224
288
  return dirname(createRequire(import.meta.url).resolve("@monimesl/dot-cli/package.json"));
225
289
  }
226
290
  //#endregion
227
- //#region src/dot-root.ts
228
- function findDotRoot(dir) {
229
- while (true) {
230
- if (existsSync(join(dir, "dot.json"))) return dir;
231
- const parent = dirname(dir);
232
- if (parent === dir) return;
233
- dir = parent;
234
- }
235
- }
236
- //#endregion
237
291
  //#region src/create/page.ts
238
292
  async function createPage(path, cwd) {
239
- if (!isPageId(path)) throw new CommandError(`"${path}" is not a page path: use letters, digits, "-" and "_", with "/" between folders, such as "cart/checkout"`);
293
+ if (!isPageId(path)) throw new CommandError(`"${path}" is not a page path. Use letters, digits, "-" and "_", with "/" between folders, such as "cart/checkout"`);
240
294
  const root = findDotRoot(resolve(cwd));
241
- if (root === void 0) throw new CommandError("There is no dot.json here or in a folder above: run it inside a dot");
242
- const dir = join(root, "pages", path);
243
- if (existsSync(dir)) throw new CommandError(`pages/${path} already exists`);
295
+ 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");
296
+ const dir = join(root, PAGES_DIR, path);
297
+ if (existsSync(dir)) throw new CommandError(`pages/${path} already exists. Choose another path`);
244
298
  await mkdir(dir, { recursive: true });
245
299
  const files = [];
246
300
  for (const [name, content] of Object.entries(PAGE_FILES)) {
@@ -251,10 +305,10 @@ async function createPage(path, cwd) {
251
305
  return {
252
306
  dir,
253
307
  files,
254
- listing: await listPage(join(root, "dot.ts"), path)
308
+ listing: await addToDotPages(join(root, DOT_FILE), path)
255
309
  };
256
310
  }
257
- async function listPage(dotFile, path) {
311
+ async function addToDotPages(dotFile, path) {
258
312
  if (!existsSync(dotFile)) return "manual";
259
313
  const source = await readFile(dotFile, "utf8");
260
314
  let dot;
@@ -264,23 +318,25 @@ async function listPage(dotFile, path) {
264
318
  return "manual";
265
319
  }
266
320
  if (dot.pages.includes(path)) return "already";
267
- const updated = withPage(source, dot.lastPage, path);
321
+ const updated = insertPage(source, dot.lastPage, path);
268
322
  if (!listsPage(updated, dotFile, path)) return "manual";
269
323
  await writeFile(dotFile, updated);
270
324
  return "added";
271
325
  }
272
- function withPage(source, lastPage, path) {
326
+ function insertPage(source, lastPage, path) {
273
327
  const quote = source[lastPage.start] === "\"" ? "\"" : "'";
274
328
  const page = `${quote}${path}${quote}`;
329
+ const before = source.slice(0, lastPage.end);
330
+ const after = source.slice(lastPage.end);
275
331
  const lineStart = source.lastIndexOf("\n", lastPage.start - 1) + 1;
276
332
  const indent = source.slice(lineStart, lastPage.start);
277
- if (!/^\s*$/.test(indent)) return `${source.slice(0, lastPage.end)}, ${page}${source.slice(lastPage.end)}`;
278
- const trailingComma = /^\s*,/.exec(source.slice(lastPage.end));
333
+ if (!/^\s*$/.test(indent)) return `${before}, ${page}${after}`;
334
+ const trailingComma = /^\s*,/.exec(after);
279
335
  if (trailingComma) {
280
336
  const end = lastPage.end + trailingComma[0].length;
281
337
  return `${source.slice(0, end)}\n${indent}${page},${source.slice(end)}`;
282
338
  }
283
- return `${source.slice(0, lastPage.end)},\n${indent}${page}${source.slice(lastPage.end)}`;
339
+ return `${before},\n${indent}${page}${after}`;
284
340
  }
285
341
  function listsPage(source, dotFile, path) {
286
342
  try {
@@ -298,15 +354,15 @@ const PACKAGE_MANAGERS = [
298
354
  "bun"
299
355
  ];
300
356
  function isPackageManager(name) {
301
- return PACKAGE_MANAGERS.includes(name);
357
+ return isOneOf(PACKAGE_MANAGERS, name);
302
358
  }
303
359
  function launchedBy(userAgent = process.env.npm_config_user_agent ?? "") {
304
360
  return PACKAGE_MANAGERS.find((name) => userAgent.startsWith(`${name}/`)) ?? "npm";
305
361
  }
306
- function installedManagers(isInstalled = onPath) {
362
+ function installedManagers(isInstalled = isOnPath) {
307
363
  return PACKAGE_MANAGERS.filter(isInstalled);
308
364
  }
309
- function onPath(name) {
365
+ function isOnPath(name) {
310
366
  return spawnSync(name, ["--version"], {
311
367
  stdio: "ignore",
312
368
  shell: process.platform === "win32"
@@ -318,9 +374,9 @@ function defaultDeveloperKeyPath() {
318
374
  return process.env.DOT_DEVELOPER_KEY ?? join(homedir(), ".dot", "developer-key.pem");
319
375
  }
320
376
  function missingDeveloperKeyHint(path = defaultDeveloperKeyPath()) {
321
- return ` This computer has no developer key in ${path}, and Dash opens only signed links.\n Make one with OpenSSL 3 (macOS's own openssl can't; brew install openssl):
377
+ return ` There is no developer key at ${path}. Dash only opens signed links.\n Make one with OpenSSL 3. macOS's built-in openssl can't, so first run brew install openssl:
322
378
 
323
- mkdir -p ${dirname(path)} && openssl genpkey -algorithm ed25519 -out ${path}\n\n Then add the public key \`dotctl keys show\` prints to your Monime account.`;
379
+ mkdir -p ${dirname(path)} && openssl genpkey -algorithm ed25519 -out ${path}\n\n Then run \`dotctl keys show\` and add the public key it prints to your Monime account.`;
324
380
  }
325
381
  async function loadDeveloperKey(path = defaultDeveloperKeyPath()) {
326
382
  let pem;
@@ -331,20 +387,22 @@ async function loadDeveloperKey(path = defaultDeveloperKeyPath()) {
331
387
  throw error;
332
388
  }
333
389
  const privateKey = createPrivateKey(pem);
334
- if (privateKey.asymmetricKeyType !== "ed25519") throw new Error(`${path} is not an Ed25519 key`);
390
+ if (privateKey.asymmetricKeyType !== "ed25519") throw new Error(`${path} is not an Ed25519 key. Make one with: openssl genpkey -algorithm ed25519 -out ${path}`);
335
391
  return toDeveloperKey(privateKey);
336
392
  }
337
393
  function toDeveloperKey(privateKey) {
338
- const raw = Buffer.from(createPublicKey(privateKey).export({ format: "jwk" }).x, "base64url");
394
+ const publicKeyJwk = createPublicKey(privateKey).export({ format: "jwk" });
395
+ const rawPublicKey = Buffer.from(publicKeyJwk.x, "base64url");
339
396
  return {
340
- publicKey: raw.toString("base64"),
341
- keyId: raw.subarray(0, 8).toString("hex"),
397
+ publicKey: rawPublicKey.toString("base64"),
398
+ keyId: rawPublicKey.subarray(0, 8).toString("hex"),
342
399
  sign: (bytes) => sign(null, bytes, privateKey).toString("base64")
343
400
  };
344
401
  }
345
402
  //#endregion
346
403
  //#region src/device/server.ts
347
404
  const DEVICE_SCHEME = "dotdev";
405
+ const FILES_PATH = "/dev/files/";
348
406
  const WAIT_MS = 25e3;
349
407
  const MAX_LOG_BYTES = 1048576;
350
408
  const LINK_LIFETIME_SECONDS = 43200;
@@ -359,133 +417,183 @@ function signedLinkMessage(server, secret, expires, keyId) {
359
417
  ].join("\n");
360
418
  }
361
419
  function deviceToken(secret) {
362
- return createHmac("sha256", Buffer.from(secret, "base64url")).update("dotdev-token", "utf8").digest("base64url");
420
+ return hmacWithSecret(secret, "dotdev-token");
363
421
  }
364
422
  function buildMac(secret, body) {
365
- return createHmac("sha256", Buffer.from(secret, "base64url")).update(`dotdev-build\n${body}`, "utf8").digest("base64url");
423
+ return hmacWithSecret(secret, `dotdev-build\n${body}`);
366
424
  }
367
425
  async function startDeviceServer({ address = localNetworkAddress(), port = 0, onLog = () => {}, onConnect = () => {}, developerKey } = {}) {
368
- if (address === void 0) throw new Error("This computer has no local network address for devices to reach it on");
369
- const secret = randomBytes(16).toString("base64url");
370
- const token = deviceToken(secret);
371
- let signature = null;
372
- let build = { files: /* @__PURE__ */ new Map() };
373
- let generation = 0;
374
- let hotSince = 0;
375
- let error = null;
376
- const waiting = /* @__PURE__ */ new Set();
377
- const connected = /* @__PURE__ */ new Set();
378
- const announce = () => {
379
- generation += 1;
380
- for (const wake of waiting) wake();
381
- };
382
- const server = createServer((request, response) => {
426
+ 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");
427
+ const deviceServer = new LocalDeviceServer(onLog, onConnect);
428
+ await deviceServer.listen(port, address, developerKey);
429
+ return deviceServer;
430
+ }
431
+ var LocalDeviceServer = class {
432
+ #onLog;
433
+ #onConnect;
434
+ #secret = randomBytes(16).toString("base64url");
435
+ #token = deviceToken(this.#secret);
436
+ #server;
437
+ #waiting = /* @__PURE__ */ new Set();
438
+ #connected = /* @__PURE__ */ new Set();
439
+ #link = "";
440
+ #signature = null;
441
+ #files = /* @__PURE__ */ new Map();
442
+ #generation = 0;
443
+ #hotSince = 0;
444
+ #error = null;
445
+ constructor(onLog, onConnect) {
446
+ this.#onLog = onLog;
447
+ this.#onConnect = onConnect;
448
+ this.#server = createServer((request, response) => {
449
+ this.#handle(request, response);
450
+ });
451
+ }
452
+ get link() {
453
+ return this.#link;
454
+ }
455
+ async listen(port, address, developerKey) {
456
+ await new Promise((resolve, reject) => {
457
+ this.#server.once("error", reject);
458
+ this.#server.listen(port, address, () => resolve());
459
+ });
460
+ const { port: listeningPort } = this.#server.address();
461
+ const origin = `http://${address}:${listeningPort}`;
462
+ if (developerKey) {
463
+ const expires = Math.floor(Date.now() / 1e3) + LINK_LIFETIME_SECONDS;
464
+ const keyId = developerKey.keyId;
465
+ const message = signedLinkMessage(origin, this.#secret, expires, keyId);
466
+ const sig = developerKey.sign(Buffer.from(message, "utf8"));
467
+ this.#signature = {
468
+ expires,
469
+ keyId,
470
+ sig
471
+ };
472
+ }
473
+ const server = encodeURIComponent(origin);
474
+ this.#link = `${DEVICE_SCHEME}://connect?server=${server}&secret=${this.#secret}`;
475
+ }
476
+ async publish(dir, { hot = false } = {}) {
477
+ this.#files = await readBuild(dir);
478
+ this.#error = null;
479
+ this.#announce();
480
+ if (!hot) this.#hotSince = this.#generation;
481
+ }
482
+ fail(message) {
483
+ this.#error = message;
484
+ this.#announce();
485
+ }
486
+ close() {
487
+ this.#wakeWaitingDevices();
488
+ return new Promise((resolve) => {
489
+ this.#server.close(() => resolve());
490
+ this.#server.closeAllConnections();
491
+ });
492
+ }
493
+ #announce() {
494
+ this.#generation += 1;
495
+ this.#wakeWaitingDevices();
496
+ }
497
+ #wakeWaitingDevices() {
498
+ for (const wake of this.#waiting) wake();
499
+ }
500
+ #handle(request, response) {
383
501
  const from = request.socket.remoteAddress ?? "a device";
384
502
  if (request.method === "GET" && request.url === "/dev/link") {
385
- if (signature) sendJson(response, signature);
386
- else response.writeHead(404).end();
503
+ this.#sendSignature(response);
387
504
  return;
388
505
  }
389
- if (!isAuthorized(request, token)) {
506
+ if (!isAuthorized(request, this.#token)) {
390
507
  response.writeHead(401).end();
391
508
  return;
392
509
  }
393
510
  const url = new URL(request.url ?? "/", "http://device");
394
511
  const path = url.pathname;
395
- if (request.method === "GET" && path === "/dev/build") {
396
- if (!connected.has(from)) {
397
- connected.add(from);
398
- onConnect(from);
399
- }
400
- const files = Object.fromEntries([...build.files].map(([name, file]) => [name, file.hash]));
401
- const body = JSON.stringify({
402
- generation,
403
- hotSince,
404
- files,
405
- error
406
- });
407
- response.writeHead(200, {
408
- "content-type": "application/json",
409
- [BUILD_MAC_HEADER]: buildMac(secret, body)
410
- }).end(body);
411
- } else if (request.method === "GET" && path.startsWith("/dev/files/")) {
412
- if (url.searchParams.get("generation") !== String(generation)) {
413
- response.writeHead(409).end();
414
- return;
415
- }
416
- const name = path.slice(11).split("/").map(decodeURIComponent).join("/");
417
- const file = build.files.get(name);
418
- if (!file) {
419
- response.writeHead(404).end();
420
- return;
421
- }
422
- response.writeHead(200, { "content-type": "application/octet-stream" }).end(file.bytes);
423
- } else if (request.method === "GET" && path === "/dev/changes") waitForChange(response, Number(url.searchParams.get("after")));
424
- else if (request.method === "POST" && path === "/dev/log") readText(request).then((text) => {
425
- for (const line of text.split("\n")) onLog(line, from);
426
- response.writeHead(204).end();
427
- }).catch(() => response.writeHead(413).end());
512
+ if (request.method === "GET" && path === "/dev/build") this.#sendBuild(response, from);
513
+ else if (request.method === "GET" && path.startsWith(FILES_PATH)) this.#sendFile(response, path, url.searchParams.get("generation"));
514
+ else if (request.method === "GET" && path === "/dev/changes") this.#waitForChange(response, Number(url.searchParams.get("after")));
515
+ else if (request.method === "POST" && path === "/dev/log") this.#receiveLog(request, response, from);
428
516
  else response.writeHead(404).end();
429
- });
430
- const waitForChange = (response, after) => {
431
- if (generation > after) {
432
- sendJson(response, { generation });
517
+ }
518
+ #sendSignature(response) {
519
+ if (this.#signature) sendJson(response, this.#signature);
520
+ else response.writeHead(404).end();
521
+ }
522
+ #sendBuild(response, from) {
523
+ if (!this.#connected.has(from)) {
524
+ this.#connected.add(from);
525
+ this.#onConnect(from);
526
+ }
527
+ const hashes = {};
528
+ for (const [name, file] of this.#files) hashes[name] = file.hash;
529
+ const body = JSON.stringify({
530
+ generation: this.#generation,
531
+ hotSince: this.#hotSince,
532
+ files: hashes,
533
+ error: this.#error
534
+ });
535
+ response.writeHead(200, {
536
+ "content-type": "application/json",
537
+ [BUILD_MAC_HEADER]: buildMac(this.#secret, body)
538
+ }).end(body);
539
+ }
540
+ #sendFile(response, path, generation) {
541
+ if (generation !== String(this.#generation)) {
542
+ response.writeHead(409).end();
433
543
  return;
434
544
  }
435
- const wake = () => {
545
+ const fileName = buildFileName(path);
546
+ if (fileName === void 0) {
547
+ response.writeHead(400).end();
548
+ return;
549
+ }
550
+ const file = this.#files.get(fileName);
551
+ if (!file) {
552
+ response.writeHead(404).end();
553
+ return;
554
+ }
555
+ response.writeHead(200, { "content-type": "application/octet-stream" }).end(file.bytes);
556
+ }
557
+ #waitForChange(response, after) {
558
+ if (this.#generation > after) {
559
+ sendJson(response, { generation: this.#generation });
560
+ return;
561
+ }
562
+ const stopWaiting = () => {
436
563
  clearTimeout(timer);
437
- waiting.delete(wake);
438
- sendJson(response, { generation });
564
+ this.#waiting.delete(wake);
565
+ };
566
+ const wake = () => {
567
+ stopWaiting();
568
+ sendJson(response, { generation: this.#generation });
439
569
  };
440
570
  const timer = setTimeout(() => {
441
- waiting.delete(wake);
571
+ stopWaiting();
442
572
  response.writeHead(204).end();
443
573
  }, WAIT_MS);
444
- waiting.add(wake);
445
- response.on("close", () => {
446
- clearTimeout(timer);
447
- waiting.delete(wake);
448
- });
449
- };
450
- await new Promise((resolve, reject) => {
451
- server.once("error", reject);
452
- server.listen(port, address, () => resolve());
453
- });
454
- const { port: listening } = server.address();
455
- const origin = `http://${address}:${listening}`;
456
- if (developerKey) {
457
- const expires = Math.floor(Date.now() / 1e3) + LINK_LIFETIME_SECONDS;
458
- const keyId = developerKey.keyId;
459
- const message = signedLinkMessage(origin, secret, expires, keyId);
460
- signature = {
461
- expires,
462
- keyId,
463
- sig: developerKey.sign(Buffer.from(message, "utf8"))
464
- };
574
+ this.#waiting.add(wake);
575
+ response.on("close", stopWaiting);
465
576
  }
466
- return {
467
- link: `${DEVICE_SCHEME}://connect?server=${encodeURIComponent(origin)}&secret=${secret}`,
468
- async publish(dir, { hot = false } = {}) {
469
- build = await readBuild(dir);
470
- error = null;
471
- announce();
472
- if (!hot) hotSince = generation;
473
- },
474
- fail(message) {
475
- error = message;
476
- announce();
477
- },
478
- close() {
479
- for (const wake of waiting) wake();
480
- return new Promise((resolve) => {
481
- server.close(() => resolve());
482
- server.closeAllConnections();
483
- });
484
- }
485
- };
577
+ #receiveLog(request, response, from) {
578
+ readLogText(request).then((text) => {
579
+ for (const line of text.split("\n")) this.#onLog(withoutControlCharacters(line), from);
580
+ response.writeHead(204).end();
581
+ }).catch(() => response.writeHead(413).end());
582
+ }
583
+ };
584
+ function hmacWithSecret(secret, text) {
585
+ const key = Buffer.from(secret, "base64url");
586
+ return createHmac("sha256", key).update(text, "utf8").digest("base64url");
486
587
  }
487
- function localNetworkAddress() {
488
- for (const addresses of Object.values(networkInterfaces())) for (const candidate of addresses ?? []) if (candidate.family === "IPv4" && !candidate.internal) return candidate.address;
588
+ const UNREACHABLE_INTERFACE = /^(bridge|utun|tun|tap|ipsec|ppp|awdl|llw|vmnet|vboxnet|docker|br-|veth|virbr|zt|tailscale|wg)/i;
589
+ function localNetworkAddress(interfaces = networkInterfaces()) {
590
+ let fallback;
591
+ for (const [name, addresses] of Object.entries(interfaces)) for (const candidate of addresses ?? []) {
592
+ if (candidate.family !== "IPv4" || candidate.internal) continue;
593
+ if (!candidate.address.startsWith("169.254.") && !UNREACHABLE_INTERFACE.test(name)) return candidate.address;
594
+ fallback ??= candidate.address;
595
+ }
596
+ return fallback;
489
597
  }
490
598
  async function readBuild(dir) {
491
599
  const files = /* @__PURE__ */ new Map();
@@ -498,28 +606,42 @@ async function readBuild(dir) {
498
606
  const path = join(entry.parentPath, entry.name);
499
607
  const bytes = await readFile(path);
500
608
  const hash = createHash("sha256").update(bytes).digest("base64url");
501
- files.set(relative(dir, path).split(sep).join("/"), {
609
+ const name = relative(dir, path).split(sep).join("/");
610
+ files.set(name, {
502
611
  bytes,
503
612
  hash
504
613
  });
505
614
  }
506
- return { files };
615
+ return files;
616
+ }
617
+ function buildFileName(path) {
618
+ const encodedParts = path.slice(11).split("/");
619
+ try {
620
+ return encodedParts.map((part) => decodeURIComponent(part)).join("/");
621
+ } catch {
622
+ return;
623
+ }
507
624
  }
508
625
  function isAuthorized(request, token) {
509
626
  const given = Buffer.from(request.headers.authorization ?? "");
510
627
  const expected = Buffer.from(`Bearer ${token}`);
511
- return given.length === expected.length && timingSafeEqual(given, expected);
628
+ if (given.length !== expected.length) return false;
629
+ return timingSafeEqual(given, expected);
512
630
  }
513
631
  function sendJson(response, body) {
514
632
  response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(body));
515
633
  }
516
- async function readText(request) {
634
+ function withoutControlCharacters(line) {
635
+ return line.replace(/[\u0000-\u0008\u000a-\u001f\u007f-\u009f]/g, "");
636
+ }
637
+ async function readLogText(request) {
517
638
  const chunks = [];
518
639
  let size = 0;
519
640
  for await (const chunk of request) {
520
- size += chunk.length;
521
- if (size > MAX_LOG_BYTES) throw new Error("Too much log at once");
522
- chunks.push(chunk);
641
+ const bytes = chunk;
642
+ size += bytes.length;
643
+ if (size > MAX_LOG_BYTES) throw new Error(`A device sent more than ${MAX_LOG_BYTES} bytes of log at once`);
644
+ chunks.push(bytes);
523
645
  }
524
646
  return Buffer.concat(chunks).toString("utf8");
525
647
  }
@@ -539,64 +661,40 @@ function startDotSimulator(project, outDir, port) {
539
661
  const CHANGE_DEBOUNCE_MS = 50;
540
662
  async function dev({ port, device, ...options }) {
541
663
  const project = await loadProject(options.root);
542
- const outDir = resolve(project.root, options.outDir ?? "dist");
664
+ const outDir = resolveOutDir(project.root, options.outDir);
543
665
  const buildOptions = {
544
666
  mode: "development",
545
667
  ...options,
546
668
  outDir
547
669
  };
548
- const buildApp = async () => {
549
- const started = performance.now();
550
- try {
551
- printWarnings(await build(buildOptions));
552
- printBuildTime(started);
553
- return null;
554
- } catch (error) {
555
- const message = describeError(error).replaceAll(project.root + sep, "");
556
- console.error(`error: ${message}`);
557
- return message;
558
- }
559
- };
560
- const firstError = await buildApp();
670
+ const firstError = await buildDot(buildOptions, project.root);
561
671
  const simulator = await startDotSimulator(project, outDir, port);
562
672
  if (firstError) simulator.error(firstError);
563
- console.log(`\n Simulator running at: ${simulator.url}\n`);
564
- let devices;
565
- if (device) {
566
- const developerKey = await loadDeveloperKey();
567
- devices = await startDeviceServer({
568
- developerKey,
569
- onConnect: (from) => console.log(`A device connected from ${from}`),
570
- onLog: (line) => console.log(`device ${line}`)
571
- });
572
- if (firstError) devices.fail(firstError);
573
- else await devices.publish(outDir);
574
- printDeviceLink(devices.link);
575
- if (developerKey) console.log(` Signed with developer key ${developerKey.keyId}\n`);
576
- else console.log(`${missingDeveloperKeyHint()}\n`);
577
- }
578
- const changed = /* @__PURE__ */ new Set();
673
+ printSimulatorUrl(simulator.url);
674
+ let deviceServer;
675
+ if (device) deviceServer = await startDevices(outDir, firstError);
676
+ const changedFiles = /* @__PURE__ */ new Set();
579
677
  const pagesDir = join(project.root, PAGES_DIR) + sep;
580
678
  const rebuilds = new RebuildQueue(async () => {
581
- const building = [...changed];
582
- const error = await buildApp();
679
+ const filesInThisBuild = [...changedFiles];
680
+ const error = await buildDot(buildOptions, project.root);
583
681
  if (error) {
584
682
  simulator.error(error);
585
- devices?.fail(error);
683
+ deviceServer?.fail(error);
586
684
  return;
587
685
  }
588
- for (const path of building) changed.delete(path);
589
- const hot = building.every((path) => path.startsWith(pagesDir));
686
+ for (const path of filesInThisBuild) changedFiles.delete(path);
687
+ const hot = filesInThisBuild.every((path) => path.startsWith(pagesDir));
590
688
  if (hot) simulator.hotReload();
591
689
  else simulator.reload();
592
- await devices?.publish(outDir, { hot });
690
+ await deviceServer?.publish(outDir, { hot });
593
691
  });
594
692
  let debounceTimer;
595
693
  const watcher = watch(project.root, { recursive: true }, (_event, file) => {
596
694
  if (!file || isIgnored(file)) return;
597
695
  const path = resolve(project.root, file);
598
696
  if (path === outDir || path.startsWith(outDir + sep)) return;
599
- changed.add(path);
697
+ changedFiles.add(path);
600
698
  clearTimeout(debounceTimer);
601
699
  debounceTimer = setTimeout(() => {
602
700
  console.log(`Changed ${relative(project.root, path)}`);
@@ -606,12 +704,39 @@ async function dev({ port, device, ...options }) {
606
704
  return async () => {
607
705
  clearTimeout(debounceTimer);
608
706
  watcher.close();
609
- await Promise.all([simulator.close(), devices?.close()]);
707
+ await Promise.all([simulator.close(), deviceServer?.close()]);
610
708
  };
611
709
  }
710
+ async function buildDot(buildOptions, root) {
711
+ const startedAt = performance.now();
712
+ try {
713
+ printWarnings(await build(buildOptions));
714
+ printBuildTime(startedAt);
715
+ return null;
716
+ } catch (error) {
717
+ const message = describeError(error).replaceAll(root + sep, "");
718
+ console.error(`error: ${message}`);
719
+ return message;
720
+ }
721
+ }
722
+ async function startDevices(outDir, firstError) {
723
+ const developerKey = await loadDeveloperKey();
724
+ const deviceServer = await startDeviceServer({
725
+ developerKey,
726
+ onConnect: (from) => console.log(`A device connected from ${from}`),
727
+ onLog: (line) => console.log(`device ${line}`)
728
+ });
729
+ if (firstError) deviceServer.fail(firstError);
730
+ else await deviceServer.publish(outDir);
731
+ printDeviceLink(deviceServer.link);
732
+ if (developerKey) console.log(` Signed with developer key ${developerKey.keyId}\n`);
733
+ else console.log(`${missingDeveloperKeyHint()}\n`);
734
+ return deviceServer;
735
+ }
612
736
  function printDeviceLink(link) {
613
- console.log(" Scan this in Dash: turn on Enable Dot Debugging in Developer Settings, then tap Live Test on\n the Dots tab.\n");
614
- console.log(renderUnicodeCompact(link, { border: 1 }).replace(/^/gm, " "));
737
+ 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");
738
+ const qrCode = renderUnicodeCompact(link, { border: 1 });
739
+ console.log(qrCode.replace(/^/gm, " "));
615
740
  console.log(`\n ${link}\n`);
616
741
  }
617
742
  var RebuildQueue = class {
@@ -649,16 +774,16 @@ function isIgnored(file) {
649
774
  //#region src/preview.ts
650
775
  async function preview({ port, ...options }) {
651
776
  const project = await loadProject(options.root);
652
- const outDir = resolve(project.root, options.outDir ?? "dist");
653
- const started = performance.now();
777
+ const outDir = resolveOutDir(project.root, options.outDir);
778
+ const startedAt = performance.now();
654
779
  printWarnings(await build({
655
780
  ...options,
656
781
  outDir,
657
782
  mode: "production"
658
783
  }));
659
- printBuildTime(started);
784
+ printBuildTime(startedAt);
660
785
  const simulator = await startDotSimulator(project, outDir, port);
661
- console.log(`\n Simulator running at: ${simulator.url}\n`);
786
+ printSimulatorUrl(simulator.url);
662
787
  return simulator;
663
788
  }
664
789
  //#endregion
@@ -670,29 +795,35 @@ const TERMINAL = {
670
795
  const ERASE_BELOW = "\x1B[0J";
671
796
  const HIDE_CURSOR = "\x1B[?25l";
672
797
  const SHOW_CURSOR = "\x1B[?25h";
673
- const cursorUp = (lines) => `\x1b[${lines}A`;
674
798
  async function select(question, choices, fallback, { input, output } = TERMINAL) {
675
799
  if (!input.isTTY) return fallback;
676
- let current = Math.max(0, choices.findIndex((choice) => choice.value === fallback));
677
- const width = choices.some((choice) => choice.hint) ? Math.max(...choices.map((choice) => choice.value.length)) : 0;
678
- const row = (choice, index) => {
679
- const name = choice.value.padEnd(width);
800
+ let selectedIndex = choices.findIndex((choice) => choice.value === fallback);
801
+ if (selectedIndex < 0) selectedIndex = 0;
802
+ let valueWidth = 0;
803
+ if (choices.some((choice) => choice.hint)) valueWidth = Math.max(...choices.map((choice) => choice.value.length));
804
+ const formatChoice = (choice, index) => {
805
+ const value = choice.value.padEnd(valueWidth);
680
806
  const hint = choice.hint ? styleText("dim", ` ${choice.hint}`) : "";
681
807
  const defaultMark = choice.value === fallback ? styleText("green", " (default)") : "";
682
- if (index === current) return `${styleText("cyan", "❯")} ${styleText(["cyan", "bold"], name)}${hint}${defaultMark}`;
683
- return ` ${name}${hint}${defaultMark}`;
808
+ if (index === selectedIndex) return `${styleText("cyan", "❯")} ${styleText(["cyan", "bold"], value)}${hint}${defaultMark}`;
809
+ return ` ${value}${hint}${defaultMark}`;
684
810
  };
685
811
  let drawnLines = 0;
686
812
  const draw = (lines) => {
687
- output.write(`${drawnLines ? cursorUp(drawnLines) : ""}${ERASE_BELOW}${lines.join("\n")}\n`);
813
+ let moveUp = "";
814
+ if (drawnLines > 0) moveUp = cursorUp(drawnLines);
815
+ output.write(`${moveUp}${ERASE_BELOW}${lines.join("\n")}\n`);
688
816
  drawnLines = lines.length;
689
817
  };
690
- const drawList = () => draw([`${styleText("bold", question)} ${styleText("dim", "↑/↓ to move, Enter to choose")}`, ...choices.map(row)]);
818
+ const drawQuestionAndChoices = () => {
819
+ const keysHint = styleText("dim", "↑/↓ to move, Enter to choose");
820
+ draw([`${styleText("bold", question)} ${keysHint}`, ...choices.map(formatChoice)]);
821
+ };
691
822
  emitKeypressEvents(input);
692
823
  input.setRawMode(true);
693
824
  input.resume();
694
825
  output.write(HIDE_CURSOR);
695
- drawList();
826
+ drawQuestionAndChoices();
696
827
  return new Promise((resolve) => {
697
828
  const onKeypress = (_text, key) => {
698
829
  if (key.ctrl && key.name === "c") {
@@ -700,13 +831,13 @@ async function select(question, choices, fallback, { input, output } = TERMINAL)
700
831
  process.exit(130);
701
832
  }
702
833
  if (key.name === "up" || key.name === "k") {
703
- current = (current - 1 + choices.length) % choices.length;
704
- drawList();
834
+ selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
835
+ drawQuestionAndChoices();
705
836
  } else if (key.name === "down" || key.name === "j") {
706
- current = (current + 1) % choices.length;
707
- drawList();
837
+ selectedIndex = (selectedIndex + 1) % choices.length;
838
+ drawQuestionAndChoices();
708
839
  } else if (key.name === "return" || key.name === "enter") {
709
- const { value } = choices[current];
840
+ const { value } = choices[selectedIndex];
710
841
  input.off("keypress", onKeypress);
711
842
  input.setRawMode(false);
712
843
  input.pause();
@@ -722,15 +853,18 @@ async function confirm(question, fallback, terminal = TERMINAL) {
722
853
  if (!terminal.input.isTTY) return false;
723
854
  return await select(question, [{ value: "Yes" }, { value: "No" }], fallback ? "Yes" : "No", terminal) === "Yes";
724
855
  }
725
- async function ask(question, fallback, { input, output } = TERMINAL) {
856
+ async function ask(question, fallback, hint, { input, output } = TERMINAL) {
726
857
  if (!input.isTTY) return fallback;
727
- const hint = fallback ? styleText("dim", ` (default: ${fallback})`) : "";
858
+ const hintText = hint ? styleText("dim", `, ${hint}`) : "";
859
+ const defaultMark = fallback ? styleText("dim", ` (default: ${fallback})`) : "";
728
860
  const readline = createInterface({
729
861
  input,
730
862
  output
731
863
  });
732
864
  try {
733
- return (await readline.question(`${styleText("bold", question)}${hint}: `)).trim() || fallback;
865
+ const trimmedAnswer = (await readline.question(`${styleText("bold", question)}${hintText}${defaultMark}: `)).trim();
866
+ if (trimmedAnswer === "") return fallback;
867
+ return trimmedAnswer;
734
868
  } catch (error) {
735
869
  if (error.name === "AbortError") return;
736
870
  throw error;
@@ -738,33 +872,39 @@ async function ask(question, fallback, { input, output } = TERMINAL) {
738
872
  readline.close();
739
873
  }
740
874
  }
875
+ function cursorUp(lines) {
876
+ return `\x1b[${lines}A`;
877
+ }
741
878
  //#endregion
742
879
  //#region src/cli.ts
743
880
  const USAGE = `Usage:
744
- dotctl create [dir] [--name <name>] [--template <starter|showcase>]
745
- [--pm <npm|pnpm|yarn|bun>]
746
- Create a dot (asks for its name and template):
747
- starter is one page to build on, showcase a page
748
- for each feature
749
- dotctl create page <path> Add a page to the dot you are in: pages/<path>/ with
750
- view.dml, state.ts and controller.ts
881
+ dotctl create [dir] [--name <name>] [--package <com.mysalone.thing>]
882
+ [--template <starter|showcase>] [--pm <npm|pnpm|yarn|bun>]
883
+ Create a dot. Asks for its name, its package name
884
+ (a reverse domain name you own) and its template:
885
+ starter is one page to build on, showcase has a
886
+ page for each feature
887
+ dotctl create page <path> Add a page to the dot you are in. Writes
888
+ pages/<path>/ with view.dml, state.ts and controller.ts
751
889
  dotctl build [root] [options] Build the minified bundles the host app runs
752
- dotctl dev [root] [options] Run in the browser simulator, rebuilding on change
753
- dotctl preview [root] [options] Build as \`build\` does, and run that build in the browser
754
- simulator, as the host app gets it
755
- dotctl keys show Show the public key and key ID of this computer's
756
- developer key, which signs the links \`dev --device\`
757
- shows, for Dash to open
758
- dotctl lsp [--stdio] Serve .dml files to an editor over the Language Server
759
- Protocol, on stdin and stdout
890
+ dotctl dev [root] [options] Run the dot in the browser simulator and rebuild on
891
+ every change
892
+ dotctl preview [root] [options] Build as \`build\` does, then run that build in the
893
+ browser simulator, exactly as the host app gets it
894
+ dotctl keys show Show this computer's developer key: its public key
895
+ and key ID. The key signs the links that
896
+ \`dev --device\` shows, so Dash will open them
897
+ dotctl lsp [--stdio] Start the .dml language server for editors. It talks
898
+ over stdin and stdout
760
899
 
761
900
  Options:
762
901
  --out-dir <dir> Output directory (default: <root>/dist)
763
- --conditions <list> Extra export conditions, comma separated
764
- --port <port> Simulator port for \`dev\` and \`preview\`; the next free one is used
765
- if taken (default: ${DEFAULT_PORT})
766
- --device With \`dev\`, also serve the dot to the Dot example apps on phones on
767
- the local network, through a link and QR code; they reload on each build
902
+ --conditions <list> Extra export conditions, separated by commas
903
+ --port <port> Simulator port for \`dev\` and \`preview\`. If it is taken, the
904
+ next free port is used (default: ${DEFAULT_PORT})
905
+ --device With \`dev\`, also serve the dot to the Dot example apps on phones
906
+ on your local network, through a link and QR code. The apps
907
+ reload after each build
768
908
  -h, --help Show this message`;
769
909
  async function main(argv) {
770
910
  const { values, positionals } = parseArgs({
@@ -776,6 +916,7 @@ async function main(argv) {
776
916
  port: { type: "string" },
777
917
  device: { type: "boolean" },
778
918
  name: { type: "string" },
919
+ package: { type: "string" },
779
920
  template: { type: "string" },
780
921
  pm: { type: "string" },
781
922
  help: {
@@ -786,59 +927,87 @@ async function main(argv) {
786
927
  }
787
928
  });
788
929
  const [command, target, pagePath] = positionals;
789
- if (command === "lsp" && !values.help) {
790
- const { serveStdio } = await import("./server-BfPAnsSf.js");
791
- serveStdio();
792
- return new Promise(() => {});
793
- }
794
- if (command === "keys" && !values.help) return keys(target);
795
- if (command === "create" && target === "page" && !values.help) return addPage(pagePath);
796
- if (command === "create" && !values.help) return createApp(target, values.name, values.template, values.pm);
797
- if (values.help || command !== "build" && command !== "dev" && command !== "preview") {
930
+ if (values.help) {
798
931
  console.log(USAGE);
799
- return values.help ? 0 : 1;
932
+ return 0;
800
933
  }
801
- const port = values.port === void 0 ? void 0 : Number(values.port);
802
- if (port !== void 0 && !Number.isInteger(port)) {
803
- console.error(`Invalid --port "${values.port}"`);
804
- return 1;
934
+ switch (command) {
935
+ case "lsp": return serveLanguageServer();
936
+ case "keys": return showKeys(target);
937
+ case "create":
938
+ if (target === "page") return addPage(pagePath);
939
+ return createDot(target, {
940
+ name: values.name,
941
+ dotPackage: values.package,
942
+ template: values.template,
943
+ packageManager: values.pm
944
+ });
945
+ case "build":
946
+ case "dev":
947
+ case "preview": break;
948
+ default:
949
+ console.log(USAGE);
950
+ return 1;
951
+ }
952
+ let port;
953
+ if (values.port !== void 0) {
954
+ port = Number(values.port);
955
+ if (!Number.isInteger(port)) {
956
+ console.error(`--port must be a whole number, not "${values.port}"`);
957
+ return 1;
958
+ }
805
959
  }
806
960
  const options = { root: target ?? "." };
807
961
  if (values["out-dir"] !== void 0) options.outDir = values["out-dir"];
808
962
  if (values.conditions !== void 0) options.conditions = values.conditions.split(",");
809
- if (command === "dev") {
810
- const stop = await dev({
811
- ...options,
812
- port,
813
- device: values.device
814
- });
815
- await new Promise((resolve) => process.once("SIGINT", resolve));
816
- await stop();
817
- return 0;
818
- }
819
- if (command === "preview") {
820
- if (values.device) {
821
- console.error("--device works only with dotctl dev");
822
- return 1;
823
- }
824
- const simulator = await preview({
825
- ...options,
826
- port
827
- });
828
- await new Promise((resolve) => process.once("SIGINT", resolve));
829
- await simulator.close();
830
- return 0;
963
+ if (command === "dev") return runDev(options, port, values.device);
964
+ if (command === "preview") return runPreview(options, port, values.device);
965
+ return runBuild(options);
966
+ }
967
+ async function serveLanguageServer() {
968
+ const { serveStdio } = await import("./server-DMnWDdJ6.js");
969
+ serveStdio();
970
+ return new Promise(() => {});
971
+ }
972
+ async function runDev(options, port, device) {
973
+ const stop = await dev({
974
+ ...options,
975
+ port,
976
+ device
977
+ });
978
+ await waitForCtrlC();
979
+ await stop();
980
+ return 0;
981
+ }
982
+ async function runPreview(options, port, device) {
983
+ if (device) {
984
+ console.error("--device only works with dotctl dev");
985
+ return 1;
831
986
  }
832
- const started = performance.now();
987
+ const simulator = await preview({
988
+ ...options,
989
+ port
990
+ });
991
+ await waitForCtrlC();
992
+ await simulator.close();
993
+ return 0;
994
+ }
995
+ async function runBuild(options) {
996
+ const startedAt = performance.now();
833
997
  const result = await build(options);
834
- for (const file of result.files.filter((file) => !file.endsWith(".map"))) console.log(` ${relative(process.cwd(), file)}`);
998
+ printFiles(result.files.filter((file) => !file.endsWith(".map")));
835
999
  printWarnings(result);
836
- printBuildTime(started);
1000
+ printBuildTime(startedAt);
837
1001
  return 0;
838
1002
  }
839
- async function keys(action) {
1003
+ function waitForCtrlC() {
1004
+ return new Promise((resolve) => {
1005
+ process.once("SIGINT", () => resolve());
1006
+ });
1007
+ }
1008
+ async function showKeys(action) {
840
1009
  if (action !== "show") {
841
- console.error("dotctl keys show");
1010
+ console.error("Usage: dotctl keys show");
842
1011
  return 1;
843
1012
  }
844
1013
  const path = defaultDeveloperKeyPath();
@@ -850,63 +1019,91 @@ async function keys(action) {
850
1019
  console.log(`\n Public key: ${key.publicKey}\n Key ID: ${key.keyId}\n`);
851
1020
  return 0;
852
1021
  }
853
- async function addPage(path) {
854
- if (path === void 0) {
855
- console.error("Which page? dotctl create page <path>, such as dotctl create page cart/checkout");
1022
+ async function addPage(typedPath) {
1023
+ if (typedPath === void 0) {
1024
+ console.error("Give the page a path, such as: dotctl create page cart/checkout");
856
1025
  return 1;
857
1026
  }
858
- if (path.startsWith("/")) path = path.substring(1);
859
- if (path.endsWith("/")) path = path.substring(0, path.length - 1);
1027
+ const path = trimSlashes(typedPath);
860
1028
  const { files, listing } = await createPage(path, process.cwd());
861
- for (const file of files) console.log(` ${relative(process.cwd(), file)}`);
1029
+ printFiles(files);
862
1030
  if (listing === "added") console.log(`\nListed '${path}' in dot.ts.`);
863
- else if (listing === "manual") console.log(`\nList it in dot.ts to build it: defineDot({ pages: [..., '${path}'] })`);
1031
+ else if (listing === "manual") console.log(`\nAdd it to the pages in dot.ts so it gets built: defineDot({ pages: [..., '${path}'] })`);
864
1032
  return 0;
865
1033
  }
866
- async function createApp(dir, name, templateFlag, packageManagerFlag) {
1034
+ function trimSlashes(path) {
1035
+ let trimmed = path;
1036
+ if (trimmed.startsWith("/")) trimmed = trimmed.substring(1);
1037
+ if (trimmed.endsWith("/")) trimmed = trimmed.substring(0, trimmed.length - 1);
1038
+ return trimmed;
1039
+ }
1040
+ async function createDot(dir, flags) {
1041
+ const templateFlag = flags.template;
867
1042
  if (templateFlag !== void 0 && !isTemplate(templateFlag)) {
868
1043
  console.error(`--template must be one of ${TEMPLATES.join(", ")}`);
869
1044
  return 1;
870
1045
  }
1046
+ const packageManagerFlag = flags.packageManager;
871
1047
  if (packageManagerFlag !== void 0 && !isPackageManager(packageManagerFlag)) {
872
1048
  console.error(`--pm must be one of ${PACKAGE_MANAGERS.join(", ")}`);
873
1049
  return 1;
874
1050
  }
875
- name ??= await ask("Dot name", dir ? toDisplayName(dir) : "Dot");
1051
+ let name = flags.name;
1052
+ if (name === void 0) name = await ask("Dot name", dir ? toDisplayName(dir) : "Dot");
876
1053
  if (!name) {
877
- console.error("A dot needs a name: dotctl create [dir] --name \"My Dot\"");
1054
+ console.error("The dot needs a name. Pass one with: dotctl create [dir] --name \"My Dot\"");
878
1055
  return 1;
879
1056
  }
880
- const template = templateFlag ?? await chooseTemplate();
1057
+ const dotPackageFlag = flags.dotPackage;
1058
+ if (dotPackageFlag !== void 0 && !isDotPackage(dotPackageFlag)) {
1059
+ console.error(`--package "${dotPackageFlag}" isn't a package name. ${DOT_PACKAGE_RULE}`);
1060
+ return 1;
1061
+ }
1062
+ let dotPackage = dotPackageFlag;
1063
+ if (dotPackage === void 0) dotPackage = await askDotPackage(suggestedDotPackage(toPackageName(name)));
1064
+ if (!dotPackage) return 1;
1065
+ let template = templateFlag;
1066
+ if (template === void 0) template = await chooseTemplate();
881
1067
  const { root, workspace } = await create({
882
1068
  name,
883
1069
  dir: dir ?? toPackageName(name),
1070
+ dotPackage,
884
1071
  template
885
1072
  });
886
- const path = relative(process.cwd(), root) || ".";
1073
+ let path = relative(process.cwd(), root);
1074
+ if (path === "") path = ".";
887
1075
  console.log(`\nCreated ${name} in ${path}`);
888
1076
  if (workspace) console.log("It is inside the Dot repository, so it uses the workspace packages.");
889
- let packageManager = workspace ? "pnpm" : packageManagerFlag ?? launchedBy();
1077
+ let packageManager;
1078
+ if (workspace) packageManager = "pnpm";
1079
+ else packageManager = packageManagerFlag ?? launchedBy();
890
1080
  console.log();
891
1081
  if (await confirm("Install and start it now?", true)) {
892
1082
  if (!workspace && !packageManagerFlag) packageManager = await choosePackageManager(packageManager);
893
- if (await run(packageManager, ["install"], root) === 0) return run(packageManager, ["run", "dev"], root);
894
- console.error(`\n${packageManager} install failed.`);
1083
+ if (await runCommand(packageManager, ["install"], root) === 0) return runCommand(packageManager, ["run", "dev"], root);
1084
+ console.error(`\n${packageManager} install failed. Fix the problem above, then run the steps below.`);
895
1085
  }
896
1086
  console.log(`\nNext:\n\n cd ${path}\n ${packageManager} install\n ${packageManager} run dev\n`);
897
1087
  return 0;
898
1088
  }
899
- function run(command, args, cwd) {
900
- return new Promise((resolvePromise, reject) => {
1089
+ function runCommand(command, args, cwd) {
1090
+ return new Promise((resolve, reject) => {
901
1091
  const child = spawn(command, args, {
902
1092
  cwd,
903
1093
  stdio: "inherit",
904
1094
  shell: process.platform === "win32"
905
1095
  });
906
1096
  child.on("error", reject);
907
- child.on("exit", (code) => resolvePromise(code ?? 1));
1097
+ child.on("exit", (exitCode) => resolve(exitCode ?? 1));
908
1098
  });
909
1099
  }
1100
+ async function askDotPackage(suggested) {
1101
+ while (true) {
1102
+ const answer = await ask("Package name", suggested, "a reverse domain name you own");
1103
+ if (answer === void 0 || isDotPackage(answer)) return answer;
1104
+ console.error(`"${answer}" isn't a package name. ${DOT_PACKAGE_RULE}`);
1105
+ }
1106
+ }
910
1107
  function chooseTemplate() {
911
1108
  console.log();
912
1109
  return select("Template?", TEMPLATES.map((template) => ({
@@ -917,10 +1114,11 @@ function chooseTemplate() {
917
1114
  async function choosePackageManager(suggested) {
918
1115
  const installed = installedManagers();
919
1116
  if (installed.length < 2) return installed[0] ?? suggested;
920
- const fallback = installed.includes(suggested) ? suggested : installed[0];
1117
+ let fallback = installed[0];
1118
+ if (installed.includes(suggested)) fallback = suggested;
921
1119
  return select("Package manager?", installed.map((manager) => ({ value: manager })), fallback);
922
1120
  }
923
- main(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
1121
+ main(process.argv.slice(2)).then((exitCode) => process.exit(exitCode), (error) => {
924
1122
  console.error(`error: ${describeError(error)}`);
925
1123
  process.exit(1);
926
1124
  });