windows-development-sandbox 1.18.4 → 1.20.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 (181) hide show
  1. package/README.md +1 -1
  2. package/config.example.json +9 -0
  3. package/dist/app/features/accounts/accounts.def.d.ts +4 -0
  4. package/dist/app/features/accounts/accounts.run.d.ts +1 -0
  5. package/dist/app/features/accounts/services.def.d.ts +3 -0
  6. package/dist/app/features/accounts/services.def.js +4 -2
  7. package/dist/app/features/accounts/services.run.d.ts +1 -0
  8. package/dist/app/features/accounts/services.run.js +3 -1
  9. package/dist/app/features/agent-commands/agent-commands.def.d.ts +37 -0
  10. package/dist/app/features/agent-commands/agent-commands.def.js +91 -0
  11. package/dist/app/features/agent-commands/agent-commands.run.d.ts +28 -0
  12. package/dist/app/features/agent-commands/agent-commands.run.js +35 -0
  13. package/dist/app/features/agent-commands/index.d.ts +2 -0
  14. package/dist/app/features/agent-commands/index.js +3 -0
  15. package/dist/app/features/agents/agents.def.d.ts +14 -1
  16. package/dist/app/features/agents/agents.def.js +61 -6
  17. package/dist/app/features/agents/agents.run.d.ts +1 -0
  18. package/dist/app/features/agents/agents.run.js +8 -1
  19. package/dist/app/features/agents/claude-env.def.d.ts +36 -0
  20. package/dist/app/features/agents/claude-env.def.js +305 -0
  21. package/dist/app/features/agents/env.def.d.ts +163 -0
  22. package/dist/app/features/agents/env.def.js +184 -0
  23. package/dist/app/features/agents/env.run.d.ts +43 -0
  24. package/dist/app/features/agents/env.run.js +86 -0
  25. package/dist/app/features/agents/index.d.ts +2 -0
  26. package/dist/app/features/agents/index.js +2 -0
  27. package/dist/app/features/defs.d.ts +132 -1
  28. package/dist/app/features/defs.js +9 -0
  29. package/dist/app/features/settings/categories.def.js +5 -2
  30. package/dist/app/features/settings/settings-rows.js +5 -1
  31. package/dist/app/features/settings/settings.run.d.ts +8 -0
  32. package/dist/app/features/settings/settings.run.js +9 -0
  33. package/dist/app/features/updates/updates.run.js +12 -2
  34. package/dist/app/features/vpn/vpn.def.d.ts +33 -0
  35. package/dist/app/features/vpn/vpn.def.js +30 -6
  36. package/dist/app/features/vpn/vpn.run.d.ts +11 -0
  37. package/dist/app/features/vpn/vpn.run.js +4 -1
  38. package/dist/app/kernel/rpc.d.ts +1 -1
  39. package/dist/app/registry.js +7 -1
  40. package/dist/bin/agent-sandbox.js +2 -2
  41. package/dist/bin/{chunk-MY7NLQQY.js → chunk-2IMT7ZCT.js} +1 -1
  42. package/dist/bin/{chunk-N4GXYTLV.js → chunk-DK4NTBUF.js} +69 -26
  43. package/dist/bin/{chunk-FRPLQLSY.js → chunk-Z2P5OGKL.js} +4323 -3463
  44. package/dist/bin/claude-host.js +2 -2
  45. package/dist/bin/claude-sbx.js +2 -2
  46. package/dist/bin/codex-sbx.js +2 -2
  47. package/dist/bin/gemini-sbx.js +2 -2
  48. package/dist/bin/sandbox.js +2 -2
  49. package/dist/bin/sbx-hide.js +2 -2
  50. package/dist/bin/sbx-show.js +2 -2
  51. package/dist/cli/commands/agent-commands.d.ts +0 -1
  52. package/dist/cli/commands/agent-commands.js +2 -32
  53. package/dist/cli/commands/index.d.ts +1 -1
  54. package/dist/cli/commands/index.js +1 -1
  55. package/dist/cli/commands/launch/launch.js +2 -2
  56. package/dist/cli/commands/registry.js +3 -2
  57. package/dist/cli/manifest.d.ts +1 -1
  58. package/dist/cli/manifest.js +1 -1
  59. package/dist/cli/menu/menu.d.ts +1 -1
  60. package/dist/cli/menu/menu.js +3 -3
  61. package/dist/cli/menu/pages/agents-page.d.ts +7 -2
  62. package/dist/cli/menu/pages/agents-page.js +67 -11
  63. package/dist/cli/menu/pages/maintenance-page.js +1 -2
  64. package/dist/cli/menu/pages/settings-page.js +1 -0
  65. package/dist/cli/menu/views.js +2 -1
  66. package/dist/cli/services/agent-commands-service.d.ts +2 -0
  67. package/dist/cli/services/agent-commands-service.js +13 -7
  68. package/dist/cli/services/vpn-service.d.ts +5 -1
  69. package/dist/cli/services/vpn-service.js +6 -1
  70. package/dist/cli/tui/dialogs/text-screen.js +2 -1
  71. package/dist/cli/tui/terminal.js +3 -1
  72. package/dist/core/config/defaults.js +9 -0
  73. package/dist/core/detect/toolchains.js +9 -0
  74. package/dist/core/disk.d.ts +1 -5
  75. package/dist/core/disk.js +1 -16
  76. package/dist/core/schema/codex.d.ts +29 -0
  77. package/dist/core/schema/codex.js +24 -0
  78. package/dist/core/schema/config.d.ts +46 -14
  79. package/dist/core/schema/config.js +2 -0
  80. package/dist/core/schema/index.d.ts +1 -0
  81. package/dist/core/schema/index.js +1 -0
  82. package/dist/core/schema/toolchains.d.ts +4 -0
  83. package/dist/core/schema/toolchains.js +3 -0
  84. package/dist/core/types.d.ts +2 -0
  85. package/dist/guest/accounts/apply.d.ts +6 -6
  86. package/dist/guest/agents/agent-files.d.ts +26 -6
  87. package/dist/guest/agents/agent-files.js +84 -14
  88. package/dist/guest/agents/local-state.d.ts +5 -2
  89. package/dist/guest/agents/local-state.js +24 -10
  90. package/dist/guest/boot/boot.js +10 -5
  91. package/dist/guest/daemon/sessions.js +2 -1
  92. package/dist/guest/toolchains/installers.d.ts +6 -10
  93. package/dist/guest/toolchains/installers.js +4 -12
  94. package/dist/guest/toolchains/native/autosdk.d.ts +3 -0
  95. package/dist/guest/toolchains/native/autosdk.js +50 -0
  96. package/dist/guest/toolchains/native/unreal.js +3 -0
  97. package/dist/guest/toolchains/native/vcruntime.d.ts +0 -3
  98. package/dist/guest/toolchains/native/vcruntime.js +2 -5
  99. package/dist/guest/toolchains/native/visual-studio.d.ts +2 -2
  100. package/dist/guest/toolchains/native/visual-studio.js +100 -16
  101. package/dist/guest/toolchains/native/vs-request.d.ts +21 -0
  102. package/dist/guest/toolchains/native/vs-request.js +60 -1
  103. package/dist/guest/toolchains/not-yet.d.ts +1 -1
  104. package/dist/guest/toolchains/not-yet.js +3 -3
  105. package/dist/guest/toolchains/persisted.js +2 -7
  106. package/dist/guest/toolchains/plan.js +9 -4
  107. package/dist/guest/toolchains/signature.d.ts +5 -0
  108. package/dist/guest/toolchains/signature.js +22 -0
  109. package/dist/guest/vpn/tunnel.d.ts +3 -0
  110. package/dist/guest/vpn/tunnel.js +10 -12
  111. package/dist/guest/vpn/vpn.js +6 -1
  112. package/dist/host/accounts/leases.d.ts +3 -3
  113. package/dist/host/accounts/source.js +1 -1
  114. package/dist/host/agent-commands/commands.js +1 -1
  115. package/dist/host/agent-commands/forwarders.d.ts +14 -2
  116. package/dist/host/agent-commands/forwarders.js +31 -13
  117. package/dist/host/agent-commands/reconcile.d.ts +2 -1
  118. package/dist/host/agent-commands/reconcile.js +7 -8
  119. package/dist/host/agent-commands/user-path.d.ts +5 -0
  120. package/dist/host/agent-commands/user-path.js +13 -0
  121. package/dist/host/launch/session-file.js +1 -0
  122. package/dist/host/profile/accounts.js +2 -4
  123. package/dist/host/profile/claude-login.d.ts +3 -2
  124. package/dist/host/profile/claude-login.js +3 -13
  125. package/dist/host/vpn/index.d.ts +2 -1
  126. package/dist/host/vpn/index.js +2 -1
  127. package/dist/host/vpn/last.d.ts +28 -0
  128. package/dist/host/vpn/last.js +43 -0
  129. package/dist/host/vpn/login.d.ts +3 -1
  130. package/dist/host/vpn/login.js +17 -3
  131. package/dist/runtime/agent.js +28 -0
  132. package/dist/runtime/daemon.js +1117 -912
  133. package/dist/runtime/db.js +30 -0
  134. package/dist/runtime/main.js +1308 -1036
  135. package/dist/runtime/netproxy.js +22 -0
  136. package/dist/runtime/tool.js +238 -208
  137. package/dist/runtime/view.js +22 -0
  138. package/dist/selftest/agent-commands.test.js +45 -7
  139. package/dist/selftest/agent-paths.test.js +1 -12
  140. package/dist/selftest/agent-settings.test.d.ts +2 -0
  141. package/dist/selftest/agent-settings.test.js +94 -0
  142. package/dist/selftest/app.test.js +2 -1
  143. package/dist/selftest/cli.test.js +4 -0
  144. package/dist/selftest/index.js +2 -0
  145. package/dist/selftest/menu.test.js +5 -3
  146. package/dist/selftest/pc-installs.test.js +5 -0
  147. package/dist/selftest/persisted-installs.test.js +69 -0
  148. package/dist/selftest/service-accounts.test.js +1 -1
  149. package/dist/selftest/support/engine-golden.js +1 -1
  150. package/dist/selftest/support/fixtures.d.ts +9 -0
  151. package/dist/selftest/vpn.test.js +51 -3
  152. package/dist/shared/accounts/anthropic.d.ts +5 -0
  153. package/dist/shared/accounts/anthropic.js +15 -0
  154. package/dist/shared/accounts/index.d.ts +1 -0
  155. package/dist/shared/accounts/index.js +1 -0
  156. package/dist/shared/channel/schema.d.ts +19 -0
  157. package/dist/shared/channel/schema.js +2 -0
  158. package/dist/shared/io/disk.d.ts +9 -0
  159. package/dist/shared/io/disk.js +19 -0
  160. package/dist/shared/io/index.d.ts +1 -0
  161. package/dist/shared/io/index.js +1 -0
  162. package/dist/shared/readiness.d.ts +9 -0
  163. package/dist/shared/readiness.js +11 -0
  164. package/dist/shared/signer.d.ts +24 -0
  165. package/dist/shared/signer.js +15 -0
  166. package/dist/shared/vpn/failure.d.ts +10 -0
  167. package/dist/shared/vpn/failure.js +28 -0
  168. package/dist/sync/watcher.js +2 -0
  169. package/dist/web/index.html +1 -1
  170. package/dist/web/{main-GVZWWP4I.js → main-OUUQYNK4.js} +75 -74
  171. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  172. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  173. package/native/csharp/HostTools/Program.cs +14 -1
  174. package/native/csharp/HostTools/UserPath.cs +17 -0
  175. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  176. package/native/scripts/edit-user-path.ps1 +6 -2
  177. package/package.json +1 -1
  178. package/sandbox/scripts/get-signature.ps1 +13 -0
  179. package/schemas/config.schema.json +64 -0
  180. package/schemas/sandbox-config.schema.json +8 -0
  181. package/sandbox/scripts/get-signer.ps1 +0 -8
@@ -93,6 +93,10 @@ export declare const ProjectSpecSchema: z.ZodObject<{
93
93
  acceptLicense: z.ZodBoolean;
94
94
  components: z.ZodArray<z.ZodString>;
95
95
  keepInstallFiles: z.ZodBoolean;
96
+ visualStudio: z.ZodEnum<{
97
+ full: "full";
98
+ autosdk: "autosdk";
99
+ }>;
96
100
  persistEngines: z.ZodBoolean;
97
101
  derivedDataCache: z.ZodEnum<{
98
102
  persisted: "persisted";
@@ -326,6 +330,10 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
326
330
  acceptLicense: z.ZodBoolean;
327
331
  components: z.ZodArray<z.ZodString>;
328
332
  keepInstallFiles: z.ZodBoolean;
333
+ visualStudio: z.ZodEnum<{
334
+ full: "full";
335
+ autosdk: "autosdk";
336
+ }>;
329
337
  persistEngines: z.ZodBoolean;
330
338
  derivedDataCache: z.ZodEnum<{
331
339
  persisted: "persisted";
@@ -579,6 +587,10 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
579
587
  acceptLicense: z.ZodBoolean;
580
588
  components: z.ZodArray<z.ZodString>;
581
589
  keepInstallFiles: z.ZodBoolean;
590
+ visualStudio: z.ZodEnum<{
591
+ full: "full";
592
+ autosdk: "autosdk";
593
+ }>;
582
594
  persistEngines: z.ZodBoolean;
583
595
  derivedDataCache: z.ZodEnum<{
584
596
  persisted: "persisted";
@@ -898,6 +910,13 @@ export declare const VpnStatusSchema: z.ZodObject<{
898
910
  up: "up";
899
911
  }>;
900
912
  message: z.ZodString;
913
+ code: z.ZodOptional<z.ZodEnum<{
914
+ auth: "auth";
915
+ unreachable: "unreachable";
916
+ driver: "driver";
917
+ timeout: "timeout";
918
+ other: "other";
919
+ }>>;
901
920
  killSwitch: z.ZodBoolean;
902
921
  killSwitchHeld: z.ZodOptional<z.ZodBoolean>;
903
922
  server: z.ZodOptional<z.ZodString>;
@@ -198,6 +198,8 @@ export const ProjectStatusSchema = z.object({
198
198
  export const VpnStatusSchema = z.object({
199
199
  state: z.enum(['off', 'starting', 'up', 'failed']),
200
200
  message: Text(2000),
201
+ /** Why it failed, when it did: `auth` (login refused), `unreachable`, `driver`, `timeout` or `other`. */
202
+ code: z.enum(['auth', 'unreachable', 'driver', 'timeout', 'other']).optional(),
201
203
  killSwitch: z.boolean(),
202
204
  /** A direct connection was tried after the kill switch was applied: false = it still got out. */
203
205
  killSwitchHeld: z.boolean().optional(),
@@ -0,0 +1,9 @@
1
+ export type StatFs = (dir: string) => {
2
+ bavail: number;
3
+ bsize: number;
4
+ };
5
+ /**
6
+ * Free bytes for this user on the drive that holds `dir` (which may not exist yet: the nearest existing parent is
7
+ * asked); null when it cannot be told. `statfs` is injectable for tests.
8
+ */
9
+ export declare function freeBytesAt(dir: string, statfs?: StatFs): number | null;
@@ -0,0 +1,19 @@
1
+ // Free room on a disk: one implementation for the PC (core/disk.ts) and the guest (toolchains/persisted.ts).
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ /**
5
+ * Free bytes for this user on the drive that holds `dir` (which may not exist yet: the nearest existing parent is
6
+ * asked); null when it cannot be told. `statfs` is injectable for tests.
7
+ */
8
+ export function freeBytesAt(dir, statfs = fs.statfsSync) {
9
+ let at = path.resolve(dir);
10
+ while (!fs.existsSync(at) && path.dirname(at) !== at)
11
+ at = path.dirname(at);
12
+ try {
13
+ const s = statfs(at);
14
+ return s.bavail * s.bsize;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
@@ -5,3 +5,4 @@ export * from './robocopy-result.js';
5
5
  export * from './copy-tree.js';
6
6
  export * from './bounded.js';
7
7
  export * from './text.js';
8
+ export * from './disk.js';
@@ -5,3 +5,4 @@ export * from './robocopy-result.js';
5
5
  export * from './copy-tree.js';
6
6
  export * from './bounded.js';
7
7
  export * from './text.js';
8
+ export * from './disk.js';
@@ -0,0 +1,9 @@
1
+ export declare const NOT_READY: {
2
+ /** Toolchains whose step is a stub: their settings rows are hidden and "auto" never turns them on. */
3
+ readonly toolchains: readonly ["unity", "godot", "android", "flutter", "dart", "reactNative", "fivem"];
4
+ /** Service sign-ins whose host or guest side is a stub: the menu and the web do not list them. */
5
+ readonly kinds: readonly ["unity", "keystore", "play", "expo", "appstore", "cfx"];
6
+ readonly unreal: readonly ["engines"];
7
+ };
8
+ export declare const isToolchainReady: (name: string) => boolean;
9
+ export declare const isKindReady: (kind: string) => boolean;
@@ -0,0 +1,11 @@
1
+ // What is not written yet, so no menu, web page or "auto" decision offers it. Each package that lands deletes its own
2
+ // entries here (the end of round 2 leaves it empty). Pure.
3
+ export const NOT_READY = {
4
+ /** Toolchains whose step is a stub: their settings rows are hidden and "auto" never turns them on. */
5
+ toolchains: ['unity', 'godot', 'android', 'flutter', 'dart', 'reactNative', 'fivem'],
6
+ /** Service sign-ins whose host or guest side is a stub: the menu and the web do not list them. */
7
+ kinds: ['unity', 'keystore', 'play', 'expo', 'appstore', 'cfx'],
8
+ unreal: ['engines'],
9
+ };
10
+ export const isToolchainReady = (name) => !NOT_READY.toolchains.includes(name);
11
+ export const isKindReady = (kind) => !NOT_READY.kinds.includes(kind);
@@ -0,0 +1,24 @@
1
+ /** Patterns (regex sources, case-insensitive) the signature's subject and issuer must match; every one of them. */
2
+ export interface AppSigner {
3
+ readonly subject: readonly string[];
4
+ readonly issuer?: readonly string[];
5
+ }
6
+ /** A valid Authenticode signature's certificate names (an invalid or missing one is reported as null). */
7
+ export interface Signature {
8
+ readonly subject: string;
9
+ readonly issuer: string;
10
+ }
11
+ /** Whether a signature is the expected signer's: every subject and issuer pattern matches. */
12
+ export declare function signerMatches(signature: Signature | null, signer: AppSigner): boolean;
13
+ /** Who signs the installers the toolchains run (the patterns the signature's subject must match). */
14
+ export declare const SIGNERS: {
15
+ readonly microsoft: {
16
+ readonly subject: readonly ["^CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US$"];
17
+ };
18
+ readonly valve: {
19
+ readonly subject: readonly ["^CN=\"?Valve(,| Corp)"];
20
+ };
21
+ readonly unity: {
22
+ readonly subject: readonly ["^CN=\"?Unity Technologies"];
23
+ };
24
+ };
@@ -0,0 +1,15 @@
1
+ // Who must sign an app's download (AppEntry.signer), for vendors that publish no checksum: the Authenticode subject
2
+ // and issuer as patterns, checked against what sandbox\scripts\get-signature.ps1 reports for a valid signature. Pure.
3
+ /** Whether a signature is the expected signer's: every subject and issuer pattern matches. */
4
+ export function signerMatches(signature, signer) {
5
+ if (!signature || !signature.subject)
6
+ return false;
7
+ const all = (text, patterns) => patterns.every((p) => new RegExp(p, 'i').test(text));
8
+ return all(signature.subject, signer.subject) && all(signature.issuer, signer.issuer ?? []);
9
+ }
10
+ /** Who signs the installers the toolchains run (the patterns the signature's subject must match). */
11
+ export const SIGNERS = {
12
+ microsoft: { subject: ['^CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US$'] },
13
+ valve: { subject: ['^CN="?Valve(,| Corp)'] },
14
+ unity: { subject: ['^CN="?Unity Technologies'] },
15
+ };
@@ -0,0 +1,10 @@
1
+ export declare const VPN_FAILURE_CODES: readonly ["auth", "unreachable", "driver", "timeout", "other"];
2
+ export type VpnFailureCode = (typeof VPN_FAILURE_CODES)[number];
3
+ export interface VpnFailure {
4
+ readonly code: VpnFailureCode;
5
+ readonly reason: string;
6
+ }
7
+ /** What the log says went wrong, or null when it says nothing (yet). */
8
+ export declare function classifyFailure(text: string): VpnFailure | null;
9
+ /** The plain words for a code, for the page ("login refused by NordVPN"). */
10
+ export declare const FAILURE_TEXT: Readonly<Record<VpnFailureCode, string>>;
@@ -0,0 +1,28 @@
1
+ // Why an OpenVPN run failed, from its log: a stable code (what the host acts on) and a plain sentence. Pure.
2
+ export const VPN_FAILURE_CODES = ['auth', 'unreachable', 'driver', 'timeout', 'other'];
3
+ /** What the log says went wrong, or null when it says nothing (yet). */
4
+ export function classifyFailure(text) {
5
+ if (/AUTH_FAILED/i.test(text))
6
+ return {
7
+ code: 'auth',
8
+ reason: 'the VPN refused the login (for NordVPN use the service credentials, not your account password)',
9
+ };
10
+ if (/TLS Error: TLS key negotiation failed|TLS handshake failed/i.test(text))
11
+ return {
12
+ code: 'unreachable',
13
+ reason: 'it could not reach the VPN server (the network blocks it, or the server is down)',
14
+ };
15
+ if (/All wintun adapters on this system are in use|Cannot open TUN\/TAP|Could not create Wintun|ovpn-dco.*(error|failed)/i.test(text))
16
+ return { code: 'driver', reason: 'the network driver for the tunnel could not be used' };
17
+ if (/Exiting due to fatal error/i.test(text))
18
+ return { code: 'other', reason: 'OpenVPN stopped with an error' };
19
+ return null;
20
+ }
21
+ /** The plain words for a code, for the page ("login refused by NordVPN"). */
22
+ export const FAILURE_TEXT = {
23
+ auth: 'login refused by the VPN',
24
+ unreachable: 'the VPN server could not be reached',
25
+ driver: 'the tunnel driver could not be used',
26
+ timeout: 'the tunnel did not come up in time',
27
+ other: 'the VPN failed',
28
+ };
@@ -14,6 +14,7 @@ import { AuditCollector, AuditLog, readAuditSettings } from '../host/audit/index
14
14
  import { readMarkedProjects, saveDataSize, watchAndMark } from '../host/guard/index.js';
15
15
  import { sizeWithin } from '../core/disk.js';
16
16
  import { autoRestartAllowed, causeSince, readHeartbeat, vanishedCause, appendHostMemory, cpuPercentBetween, KeepAwake, LifeRecord, HostMemoryStream, readHostMemoryAsync, readLoadSettings, REQUEST_SECONDS, WATCHDOG_START, watchdogStep, saveCrashReport, SessionLedger, } from '../host/life/index.js';
17
+ import { recordLastVpn } from '../host/vpn/index.js';
17
18
  import { SandboxChannel } from '../host/channel/index.js';
18
19
  import { ProjectRegistry } from '../host/projects/registry.js';
19
20
  import { ContainedStorage, setNameOf, storageOf } from '../host/storage/index.js';
@@ -276,6 +277,7 @@ export class Watcher {
276
277
  catch (e) {
277
278
  this.note('repo guard failed: ' + errMsg(e));
278
279
  }
280
+ recordLastVpn(this.storageRoot, this.dataPath);
279
281
  try {
280
282
  const s = syncBack(this.dataPath, final);
281
283
  if (s.history || final)
@@ -6,7 +6,7 @@
6
6
  <title>Sandbox</title>
7
7
  <link rel="icon" href="data:," />
8
8
  <link rel="stylesheet" href="/main-XT7T6ODM.css" />
9
- <script type="module" src="/main-GVZWWP4I.js"></script>
9
+ <script type="module" src="/main-OUUQYNK4.js"></script>
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>