@monimesl/dot-cli 0.1.0-alpha.8 → 1.0.0-beta.0

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