@rubytech/create-maxy-code 0.1.194 → 0.1.196

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 (84) hide show
  1. package/dist/__tests__/samba-provision.test.js +40 -16
  2. package/dist/index.js +37 -18
  3. package/dist/samba-provision.js +11 -8
  4. package/dist/uninstall.js +19 -3
  5. package/package.json +1 -1
  6. package/payload/platform/plugins/docs/references/access-control.md +4 -0
  7. package/payload/platform/plugins/docs/references/deployment.md +1 -19
  8. package/payload/platform/plugins/docs/references/samba.md +80 -0
  9. package/payload/platform/plugins/docs/references/troubleshooting.md +14 -0
  10. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +0 -3
  11. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  12. package/payload/platform/services/claude-session-manager/dist/http-server.js +0 -248
  13. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  14. package/payload/server/public/assets/AdminShell-DtABh34v.js +1 -0
  15. package/payload/server/public/assets/{Checkbox-G-kyM1P1.js → Checkbox-DTxJvyG7.js} +1 -1
  16. package/payload/server/public/assets/{admin-B-GDNkoN.js → admin-tKgLki8m.js} +1 -1
  17. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-BGgdXdD2.js → architectureDiagram-Q4EWVU46-CrQJbK3h.js} +1 -1
  18. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-DKWoL4Rp.js → blockDiagram-DXYQGD6D-BLCbhL8K.js} +1 -1
  19. package/payload/server/public/assets/{brand-MbpD2Bhr.css → brand-lZz3OVIE.css} +1 -1
  20. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-Ca6rs6F4.js → c4Diagram-AHTNJAMY-X34EilDe.js} +1 -1
  21. package/payload/server/public/assets/channel-ChZECiuj.js +1 -0
  22. package/payload/server/public/assets/{chunk-336JU56O-DcdMZImV.js → chunk-336JU56O-CeRuGBQ2.js} +2 -2
  23. package/payload/server/public/assets/{chunk-426QAEUC-9FSP-c1z.js → chunk-426QAEUC-CUZOrcMu.js} +1 -1
  24. package/payload/server/public/assets/{chunk-4TB4RGXK-CC1QpEvA.js → chunk-4TB4RGXK-CdrIHRcy.js} +1 -1
  25. package/payload/server/public/assets/{chunk-5FUZZQ4R-o1b70dEE.js → chunk-5FUZZQ4R-CenIbTIp.js} +1 -1
  26. package/payload/server/public/assets/{chunk-5PVQY5BW-C7VCYRz9.js → chunk-5PVQY5BW-DGfzIUel.js} +1 -1
  27. package/payload/server/public/assets/{chunk-EDXVE4YY-B8d5xj_4.js → chunk-EDXVE4YY-DSft5oIH.js} +1 -1
  28. package/payload/server/public/assets/{chunk-ENJZ2VHE-SSbr4n2X.js → chunk-ENJZ2VHE-Dn8FFfaI.js} +1 -1
  29. package/payload/server/public/assets/{chunk-ICPOFSXX-Cgj9YSpX.js → chunk-ICPOFSXX-JrwcKV7r.js} +1 -1
  30. package/payload/server/public/assets/{chunk-OYMX7WX6-DNEEcCqI.js → chunk-OYMX7WX6-C0X2fcAm.js} +1 -1
  31. package/payload/server/public/assets/{chunk-U2HBQHQK-DuaoK83q.js → chunk-U2HBQHQK-DPdLe-f5.js} +1 -1
  32. package/payload/server/public/assets/{chunk-X2U36JSP-CzMp0dpq.js → chunk-X2U36JSP-Bw8pKkFu.js} +1 -1
  33. package/payload/server/public/assets/{chunk-YZCP3GAM-B2wBDwSa.js → chunk-YZCP3GAM-DcAJR-EA.js} +1 -1
  34. package/payload/server/public/assets/{chunk-ZZ45TVLE-DkyF7XKW.js → chunk-ZZ45TVLE-DU374ZbR.js} +1 -1
  35. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BpMBCYn6.js +1 -0
  36. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BU3HoIr1.js +1 -0
  37. package/payload/server/public/assets/clone-C3j5j6ku.js +1 -0
  38. package/payload/server/public/assets/{dagre-AGj1P0p4.js → dagre-DisbqNSB.js} +1 -1
  39. package/payload/server/public/assets/{dagre-KV5264BT-Cnrxcu6A.js → dagre-KV5264BT-D_n7R7V9.js} +1 -1
  40. package/payload/server/public/assets/{data-Bg0Frxt3.js → data-DwY26t_T.js} +1 -1
  41. package/payload/server/public/assets/{diagram-5BDNPKRD-gyFYSx73.js → diagram-5BDNPKRD-CJID_5OR.js} +1 -1
  42. package/payload/server/public/assets/{diagram-G4DWMVQ6-DFmZEv5C.js → diagram-G4DWMVQ6-ByepjvdE.js} +1 -1
  43. package/payload/server/public/assets/{diagram-MMDJMWI5-DA4qbuEW.js → diagram-MMDJMWI5-DTkAHPHE.js} +1 -1
  44. package/payload/server/public/assets/{diagram-TYMM5635-D4n-8gig.js → diagram-TYMM5635-CnVRa9NJ.js} +1 -1
  45. package/payload/server/public/assets/{erDiagram-SMLLAGMA-CigRMjF1.js → erDiagram-SMLLAGMA-CRAGu7nV.js} +1 -1
  46. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-ClS27acA.js → flowDiagram-DWJPFMVM-BJusdlRz.js} +1 -1
  47. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-Dr1IulXe.js → ganttDiagram-T4ZO3ILL-BFMOBBnF.js} +1 -1
  48. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-lwLibdPs.js → gitGraphDiagram-UUTBAWPF-CGWEzh7u.js} +1 -1
  49. package/payload/server/public/assets/{graph-CpozZ74I.js → graph-D-iQdRDk.js} +1 -1
  50. package/payload/server/public/assets/{graph-labels-C2y666R0.js → graph-labels-u2_3Eyfh.js} +1 -1
  51. package/payload/server/public/assets/{graphlib-DBvoJMrc.js → graphlib-DNUvypOi.js} +1 -1
  52. package/payload/server/public/assets/{infoDiagram-42DDH7IO-D4kReEIV.js → infoDiagram-42DDH7IO-CBDrnRTc.js} +1 -1
  53. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-BnteQRQ0.js → ishikawaDiagram-UXIWVN3A-C7NwSQo_.js} +1 -1
  54. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-CXu6hLyJ.js → journeyDiagram-VCZTEJTY-DSaP7A3F.js} +1 -1
  55. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-CGDW2jle.js → kanban-definition-6JOO6SKY-DmixHCD4.js} +1 -1
  56. package/payload/server/public/assets/{line-pdg5hxC3.js → line-CGGoTheL.js} +1 -1
  57. package/payload/server/public/assets/{mermaid-parser.core-o3lIIK3Z.js → mermaid-parser.core-BudFADW7.js} +1 -1
  58. package/payload/server/public/assets/{mermaid.core-DLR6Q3rV.js → mermaid.core-BBbdJxXY.js} +3 -3
  59. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CH4cB0Dn.js → mindmap-definition-QFDTVHPH-CUiD9km1.js} +1 -1
  60. package/payload/server/public/assets/{pieDiagram-DEJITSTG-Ba4voL_Y.js → pieDiagram-DEJITSTG-BlAWpMr8.js} +1 -1
  61. package/payload/server/public/assets/{public-C8mil9h0.js → public-CjeEF2vf.js} +3 -3
  62. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-Cqn2xehe.js → quadrantDiagram-34T5L4WZ-CXmIwc4W.js} +1 -1
  63. package/payload/server/public/assets/{requirementDiagram-MS252O5E-DLfNx99l.js → requirementDiagram-MS252O5E-D_7_OeMC.js} +1 -1
  64. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-uck_V-cb.js → sankeyDiagram-XADWPNL6-BKm_PqzT.js} +1 -1
  65. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-DmQalqud.js → sequenceDiagram-FGHM5R23-CBSR0Zst.js} +1 -1
  66. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DUdWeVRa.js → stateDiagram-FHFEXIEX-CHNpT69M.js} +1 -1
  67. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Kc8XO6yH.js +1 -0
  68. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BgL1wM5n.js → timeline-definition-GMOUNBTQ-CxZoew5-.js} +1 -1
  69. package/payload/server/public/assets/{useSelectionMode-BLpVlOgm.js → useSelectionMode-CwxXQzEI.js} +1 -1
  70. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-7FHjIqPi.js → vennDiagram-DHZGUBPP-CKJdFDcG.js} +1 -1
  71. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BHzo4V1J.js → wardleyDiagram-NUSXRM2D-C-XBcm-g.js} +1 -1
  72. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DtuWhqwh.js → xychartDiagram-5P7HB3ND-Crb-ZtaP.js} +1 -1
  73. package/payload/server/public/data.html +5 -5
  74. package/payload/server/public/graph.html +6 -6
  75. package/payload/server/public/index.html +6 -6
  76. package/payload/server/public/public.html +5 -5
  77. package/payload/server/server.js +106 -38
  78. package/payload/server/public/assets/AdminShell-BoJbnn6d.js +0 -1
  79. package/payload/server/public/assets/channel-BwsV7WTV.js +0 -1
  80. package/payload/server/public/assets/classDiagram-6PBFFD2Q-CEWNfHVo.js +0 -1
  81. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BBjzRxux.js +0 -1
  82. package/payload/server/public/assets/clone-CXtSonnn.js +0 -1
  83. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DNTpFwdy.js +0 -1
  84. /package/payload/server/public/assets/{brand-CYcGHhlS.js → brand-CpGp9OyK.js} +0 -0
@@ -62,7 +62,7 @@ test("pickLanInterface: returns null when the only non-loopback iface has no IPv
62
62
  // renderBrandStanza — exact spec match
63
63
  // ---------------------------------------------------------------------------
64
64
  test("renderBrandStanza emits the spec directives in order", () => {
65
- const stanza = renderBrandStanza({ brand: "maxy-code", sharePath: "/home/admin/maxy-code" });
65
+ const stanza = renderBrandStanza({ brand: "maxy-code", sharePath: "/home/admin/maxy-code", installOwner: "admin" });
66
66
  assert.equal(stanza, [
67
67
  "[maxy-code]",
68
68
  " path = /home/admin/maxy-code",
@@ -76,11 +76,34 @@ test("renderBrandStanza emits the spec directives in order", () => {
76
76
  ].join("\n"));
77
77
  });
78
78
  test("renderBrandStanza handles realagent-code with its own share path", () => {
79
- const stanza = renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code" });
79
+ const stanza = renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" });
80
80
  assert.match(stanza, /^\[realagent-code\]\n/);
81
81
  assert.match(stanza, /\n path = \/home\/admin\/realagent-code\n/);
82
82
  assert.match(stanza, /\n force user = admin\n/);
83
83
  });
84
+ test("renderBrandStanza substitutes a non-admin install owner into valid users and force user", () => {
85
+ // Laptop case (Task 534): the install owner is the operator's Unix user
86
+ // (`neo` etc.), not the literal `admin`. The same renderer must produce a
87
+ // stanza whose `valid users` and `force user` name that owner, so SMB
88
+ // clients authenticate as a Unix user that actually exists on the device.
89
+ const stanza = renderBrandStanza({ brand: "maxy-code", sharePath: "/home/neo/maxy-code", installOwner: "neo" });
90
+ assert.match(stanza, /\n valid users = neo\n/);
91
+ assert.match(stanza, /\n force user = neo\n/);
92
+ assert.doesNotMatch(stanza, /valid users = admin/);
93
+ assert.doesNotMatch(stanza, /force user = admin/);
94
+ });
95
+ test("mergeSmbConf preserves a peer brand stanza when the new install owner differs", () => {
96
+ // Peer-brand isolation must hold across owners: a laptop install
97
+ // (installOwner=neo) adding its `[maxy-code]` stanza to a conf that
98
+ // already holds a peer `[realagent-code]` stanza provisioned for owner
99
+ // `admin` must leave the peer stanza byte-identical. The new stanza
100
+ // carries the laptop owner; the peer keeps its own. Task 534.
101
+ const peerStanza = renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" });
102
+ const existing = renderGlobalSection({ lanInterface: "eth0" }) + peerStanza;
103
+ const merged = mergeSmbConf({ existing, brand: "maxy-code", sharePath: "/home/neo/maxy-code", lanInterface: "wlan0", installOwner: "neo" });
104
+ assert.ok(merged.includes(peerStanza), "peer brand stanza must survive unchanged");
105
+ assert.match(merged, /\[maxy-code\][\s\S]+valid users = neo[\s\S]+force user = neo/);
106
+ });
84
107
  // ---------------------------------------------------------------------------
85
108
  // renderGlobalSection — LAN-only enforcement
86
109
  // ---------------------------------------------------------------------------
@@ -96,7 +119,7 @@ test("renderGlobalSection binds to lo + LAN interface only", () => {
96
119
  // renderFullSmbConf — composition order
97
120
  // ---------------------------------------------------------------------------
98
121
  test("renderFullSmbConf places globals before the brand stanza", () => {
99
- const conf = renderFullSmbConf({ brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0" });
122
+ const conf = renderFullSmbConf({ brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
100
123
  const globalIdx = conf.indexOf("[global]");
101
124
  const brandIdx = conf.indexOf("[maxy-code]");
102
125
  assert.ok(globalIdx >= 0 && brandIdx > globalIdx, "[global] must come before [maxy-code]");
@@ -105,13 +128,13 @@ test("renderFullSmbConf places globals before the brand stanza", () => {
105
128
  // mergeSmbConf — idempotency and section preservation
106
129
  // ---------------------------------------------------------------------------
107
130
  test("mergeSmbConf: empty existing conf gets globals + brand stanza", () => {
108
- const merged = mergeSmbConf({ existing: "", brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0" });
131
+ const merged = mergeSmbConf({ existing: "", brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
109
132
  assert.match(merged, /\[global\][\s\S]+\[maxy-code\]/);
110
133
  assert.match(merged, /interfaces = lo wlan0/);
111
134
  });
112
135
  test("mergeSmbConf: idempotent on second application", () => {
113
- const once = mergeSmbConf({ existing: "", brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0" });
114
- const twice = mergeSmbConf({ existing: once, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0" });
136
+ const once = mergeSmbConf({ existing: "", brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
137
+ const twice = mergeSmbConf({ existing: once, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
115
138
  assert.equal(twice, once, "second merge must produce byte-identical output");
116
139
  });
117
140
  test("mergeSmbConf: replaces an existing global section in place, preserving peer stanzas", () => {
@@ -125,26 +148,26 @@ test("mergeSmbConf: replaces an existing global section in place, preserving pee
125
148
  " guest ok = yes",
126
149
  "",
127
150
  ].join("\n");
128
- const merged = mergeSmbConf({ existing, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0" });
151
+ const merged = mergeSmbConf({ existing, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
129
152
  assert.match(merged, /interfaces = lo wlan0/);
130
153
  assert.doesNotMatch(merged, /workgroup = OLDGROUP/);
131
154
  assert.match(merged, /\[printers\][\s\S]+path = \/var\/spool\/samba/, "[printers] stanza must survive");
132
155
  assert.match(merged, /\[maxy-code\][\s\S]+path = \/home\/admin\/maxy-code/);
133
156
  });
134
157
  test("mergeSmbConf: replaces an existing brand stanza in place, leaves peer brand alone", () => {
135
- const peerStanza = renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code" });
158
+ const peerStanza = renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" });
136
159
  const oldBrandStanza = "[maxy-code]\n path = /OLD/PATH\n read only = yes\n\n";
137
160
  const existing = renderGlobalSection({ lanInterface: "eth0" }) + oldBrandStanza + peerStanza;
138
- const merged = mergeSmbConf({ existing, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0" });
161
+ const merged = mergeSmbConf({ existing, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
139
162
  assert.doesNotMatch(merged, /\/OLD\/PATH/, "old maxy-code path must be replaced");
140
163
  assert.doesNotMatch(merged, /read only = yes/, "old maxy-code directives must be replaced");
141
164
  assert.match(merged, /\[realagent-code\][\s\S]+path = \/home\/admin\/realagent-code/, "peer brand stanza must survive");
142
165
  assert.match(merged, /\[maxy-code\][\s\S]+path = \/home\/admin\/maxy-code/);
143
166
  });
144
167
  test("mergeSmbConf: appends brand stanza when not present, keeps existing peer stanzas", () => {
145
- const peerStanza = renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code" });
168
+ const peerStanza = renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" });
146
169
  const existing = renderGlobalSection({ lanInterface: "eth0" }) + peerStanza;
147
- const merged = mergeSmbConf({ existing, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0" });
170
+ const merged = mergeSmbConf({ existing, brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
148
171
  assert.match(merged, /\[realagent-code\]/, "peer brand stanza must survive");
149
172
  assert.match(merged, /\[maxy-code\][\s\S]+path = \/home\/admin\/maxy-code/, "new brand stanza must be appended");
150
173
  });
@@ -153,10 +176,11 @@ test("mergeSmbConf: appends brand stanza when not present, keeps existing peer s
153
176
  // ---------------------------------------------------------------------------
154
177
  test("removeBrandStanza: removes only this brand, leaves peer brand and global intact", () => {
155
178
  const conf = mergeSmbConf({
156
- existing: renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code" }),
179
+ existing: renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" }),
157
180
  brand: "maxy-code",
158
181
  sharePath: "/home/admin/maxy-code",
159
182
  lanInterface: "wlan0",
183
+ installOwner: "admin",
160
184
  });
161
185
  const after = removeBrandStanza({ existing: conf, brand: "maxy-code" });
162
186
  assert.doesNotMatch(after, /\[maxy-code\]/);
@@ -164,7 +188,7 @@ test("removeBrandStanza: removes only this brand, leaves peer brand and global i
164
188
  assert.match(after, /\[global\]/);
165
189
  });
166
190
  test("removeBrandStanza: returns input unchanged when stanza is absent", () => {
167
- const conf = mergeSmbConf({ existing: "", brand: "realagent-code", sharePath: "/home/admin/realagent-code", lanInterface: "wlan0" });
191
+ const conf = mergeSmbConf({ existing: "", brand: "realagent-code", sharePath: "/home/admin/realagent-code", lanInterface: "wlan0", installOwner: "admin" });
168
192
  const after = removeBrandStanza({ existing: conf, brand: "maxy-code" });
169
193
  assert.equal(after, conf);
170
194
  });
@@ -177,12 +201,12 @@ test("hasAnyBrandStanza: false when only [global] is present", () => {
177
201
  });
178
202
  test("hasAnyBrandStanza: true when any non-global stanza is present", () => {
179
203
  const conf = renderGlobalSection({ lanInterface: "wlan0" }) +
180
- renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code" });
204
+ renderBrandStanza({ brand: "realagent-code", sharePath: "/home/admin/realagent-code", installOwner: "admin" });
181
205
  assert.equal(hasAnyBrandStanza(conf), true);
182
206
  });
183
207
  test("hasAnyBrandStanza: false when both brand stanzas have been removed", () => {
184
- let conf = renderFullSmbConf({ brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0" });
185
- conf = mergeSmbConf({ existing: conf, brand: "realagent-code", sharePath: "/home/admin/realagent-code", lanInterface: "wlan0" });
208
+ let conf = renderFullSmbConf({ brand: "maxy-code", sharePath: "/home/admin/maxy-code", lanInterface: "wlan0", installOwner: "admin" });
209
+ conf = mergeSmbConf({ existing: conf, brand: "realagent-code", sharePath: "/home/admin/realagent-code", lanInterface: "wlan0", installOwner: "admin" });
186
210
  conf = removeBrandStanza({ existing: conf, brand: "maxy-code" });
187
211
  conf = removeBrandStanza({ existing: conf, brand: "realagent-code" });
188
212
  assert.equal(hasAnyBrandStanza(conf), false);
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import { classifyPortHolder } from "./preflight-port-classifier.js";
18
18
  import { parsePluginList, computeInstallActions, computeConfigureActions, parseExternalPlugins, } from "./lib/plugin-install.js";
19
19
  import { findPremiumMcpDirs } from "./lib/premium-mcp-discover.js";
20
20
  import { pickLanInterface, mergeSmbConf, formatSambaMarker, } from "./samba-provision.js";
21
- import { networkInterfaces } from "node:os";
21
+ import { networkInterfaces, userInfo } from "node:os";
22
22
  const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
23
23
  // Brand manifest — read from payload to derive all brand-specific installation values.
24
24
  // The bundler stamps brand.json into the payload at build time.
@@ -3371,12 +3371,16 @@ WantedBy=multi-user.target
3371
3371
  // marker:
3372
3372
  // apt — install the `samba` package
3373
3373
  // conf — write the brand stanza + LAN-only globals to /etc/samba/smb.conf
3374
- // user — smbpasswd the admin Linux user; deferred at install time on fresh
3375
- // Pi installs (no plaintext PIN until the operator runs set-pin)
3374
+ // user — smbpasswd the install-owner Linux user; deferred at install time
3375
+ // on fresh Pi installs (no plaintext PIN until the operator runs set-pin)
3376
3376
  // units — systemctl enable --now smbd nmbd
3377
3377
  //
3378
3378
  // The platform's set-pin route handler closes the deferral loop by running
3379
- // `smbpasswd -a admin` inline whenever the PIN is set or rotated.
3379
+ // `smbpasswd -a -s <install-owner>` inline whenever the PIN is set or
3380
+ // rotated. Install owner is the Unix user the installer is running as —
3381
+ // `admin` on a Pi/Hetzner box, `neo` (etc.) on a self-hosted laptop. It is
3382
+ // persisted to `~/.<brand>/.install-owner` so every later read uses the same
3383
+ // value the installer wrote.
3380
3384
  // ---------------------------------------------------------------------------
3381
3385
  function emitSambaMarker(step, state) {
3382
3386
  const line = formatSambaMarker(step, state);
@@ -3390,6 +3394,16 @@ function provisionSamba() {
3390
3394
  }
3391
3395
  const brand = BRAND.hostname;
3392
3396
  const sharePath = INSTALL_DIR;
3397
+ // Install owner — the Unix user that owns this brand's install on the
3398
+ // device. On a Pi/Hetzner box this is `admin`; on a self-hosted laptop it
3399
+ // is the operator's Unix user (`neo` etc.). Persisted to a file next to
3400
+ // the other install identity (`.neo4j-password`, `.admin-pin`) so the
3401
+ // platform's set-pin route and the uninstall path read the same value the
3402
+ // installer wrote, rather than re-detecting at request time. Task 534.
3403
+ const installOwner = userInfo().username;
3404
+ const installOwnerPersistDir = resolve(process.env.HOME ?? "/root", BRAND.configDir);
3405
+ mkdirSync(installOwnerPersistDir, { recursive: true });
3406
+ writeFileSync(join(installOwnerPersistDir, ".install-owner"), `${installOwner}\n`, { mode: 0o644 });
3393
3407
  // Step 1 — apt install samba. Precheck `dpkg -s samba` first; if the
3394
3408
  // package is already installed, skip apt entirely so the install does not
3395
3409
  // contend with `unattended-upgrades` for `/var/lib/dpkg/lock-frontend`
@@ -3426,7 +3440,7 @@ function provisionSamba() {
3426
3440
  }
3427
3441
  catch { /* treat as empty */ }
3428
3442
  }
3429
- const merged = mergeSmbConf({ existing, brand, sharePath, lanInterface });
3443
+ const merged = mergeSmbConf({ existing, brand, sharePath, lanInterface, installOwner });
3430
3444
  try {
3431
3445
  const tee = spawnSync("sudo", ["tee", SMB_CONF], { input: merged, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10_000 });
3432
3446
  if (tee.status !== 0) {
@@ -3436,15 +3450,18 @@ function provisionSamba() {
3436
3450
  if (testparm.status !== 0) {
3437
3451
  throw new Error(`testparm rejected merged smb.conf: ${(testparm.stderr ?? "").trim()}`);
3438
3452
  }
3439
- // Grant passwordless sudo to the admin user for the two smbpasswd
3440
- // invocations the platform's set-pin route makes (add-user-with-stdin
3441
- // and remove-user). Without this, the user systemd-unit-spawned admin
3442
- // server cannot sync the SMB password when the operator sets/rotates
3443
- // the PIN. Scoped tight: only these two literal arg-vectors are
3444
- // permitted; every other smbpasswd flavour still prompts.
3453
+ // Grant passwordless sudo to the install-owner Unix user for the two
3454
+ // smbpasswd invocations the platform's set-pin route makes (add-user-
3455
+ // with-stdin and remove-user). Without this, the user systemd-unit-
3456
+ // spawned admin server cannot sync the SMB password when the operator
3457
+ // sets/rotates the PIN. Scoped tight: only these two literal arg-vectors
3458
+ // are permitted; every other smbpasswd flavour still prompts. Principal
3459
+ // and `-a -s <user>` / `-x <user>` arguments are the install owner, not
3460
+ // a literal `admin`, so the grant works on a Pi (owner=admin) and on a
3461
+ // laptop (owner=neo) without divergence. Task 534.
3445
3462
  const SUDOERS = "/etc/sudoers.d/maxy-samba";
3446
3463
  const sudoersBody = "# maxy-code Samba provisioning (smbpasswd sync from platform set-pin route).\n" +
3447
- "admin ALL=(root) NOPASSWD: /usr/bin/smbpasswd -a -s admin, /usr/bin/smbpasswd -x admin\n";
3464
+ `${installOwner} ALL=(root) NOPASSWD: /usr/bin/smbpasswd -a -s ${installOwner}, /usr/bin/smbpasswd -x ${installOwner}\n`;
3448
3465
  const sudoersTee = spawnSync("sudo", ["tee", SUDOERS], { input: sudoersBody, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5_000 });
3449
3466
  if (sudoersTee.status !== 0) {
3450
3467
  throw new Error(`sudo tee ${SUDOERS} exited ${sudoersTee.status}: ${(sudoersTee.stderr ?? "").trim()}`);
@@ -3454,18 +3471,20 @@ function provisionSamba() {
3454
3471
  if (visudoCheck.status !== 0) {
3455
3472
  throw new Error(`visudo rejected ${SUDOERS}: ${(visudoCheck.stderr ?? "").trim()}`);
3456
3473
  }
3457
- emitSambaMarker("conf", `ok lan=${lanInterface}`);
3474
+ emitSambaMarker("conf", `ok lan=${lanInterface} owner=${installOwner}`);
3458
3475
  }
3459
3476
  catch (err) {
3460
3477
  const stderr = err instanceof Error ? err.message : String(err);
3461
3478
  emitSambaMarker("conf", `fail: ${stderr}`);
3462
3479
  throw err;
3463
3480
  }
3464
- // Step 3 — smbpasswd for the admin user. At install time the plaintext PIN
3465
- // is not in any file on disk (users.json stores the SHA-256 hash only); the
3466
- // platform's set-pin route is responsible for syncing on PIN set/rotate.
3467
- // Mark as deferred so the four-marker contract still fires unbroken.
3468
- emitSambaMarker("user", "deferred reason=no-plaintext-pin-at-install (set-pin route syncs)");
3481
+ // Step 3 — smbpasswd for the install-owner Unix user. At install time the
3482
+ // plaintext PIN is not in any file on disk (users.json stores the SHA-256
3483
+ // hash only); the platform's set-pin route is responsible for syncing on
3484
+ // PIN set/rotate. Mark as deferred so the four-marker contract still fires
3485
+ // unbroken. Owner is recorded in the marker state so the install log shows
3486
+ // which Unix user the smbpasswd entry will target. Task 534.
3487
+ emitSambaMarker("user", `deferred reason=no-plaintext-pin-at-install owner=${installOwner} (set-pin route syncs)`);
3469
3488
  // Step 4 — enable + start smbd and nmbd.
3470
3489
  try {
3471
3490
  shell("systemctl", ["enable", "--now", "smbd", "nmbd"], { sudo: true, timeout: 30_000 });
@@ -19,17 +19,20 @@
19
19
  // ---------------------------------------------------------------------------
20
20
  /**
21
21
  * Render the `[<brand>]` share stanza. Exact directives are the task's spec
22
- * verbatim: share rooted at `sharePath`, writable by `admin` only, force-uid
23
- * to admin so files created via SMB are owned by the same uid as files
24
- * created over SSH, and standard create/dir masks for an ext4 home directory.
22
+ * verbatim: share rooted at `sharePath`, writable by `installOwner` only,
23
+ * force-uid to `installOwner` so files created via SMB are owned by the same
24
+ * uid as files created over SSH, and standard create/dir masks for an ext4
25
+ * home directory. `installOwner` is the Unix user that owns this brand's
26
+ * install on the device — `admin` on a Pi or Hetzner box, the operator's
27
+ * Unix user on a self-hosted laptop. Task 534.
25
28
  */
26
29
  export function renderBrandStanza(input) {
27
30
  return [
28
31
  `[${input.brand}]`,
29
32
  ` path = ${input.sharePath}`,
30
33
  ` read only = no`,
31
- ` valid users = admin`,
32
- ` force user = admin`,
34
+ ` valid users = ${input.installOwner}`,
35
+ ` force user = ${input.installOwner}`,
33
36
  ` browseable = yes`,
34
37
  ` create mask = 0664`,
35
38
  ` directory mask = 0775`,
@@ -69,7 +72,7 @@ export function renderGlobalSection(input) {
69
72
  */
70
73
  export function renderFullSmbConf(input) {
71
74
  return renderGlobalSection({ lanInterface: input.lanInterface }) +
72
- renderBrandStanza({ brand: input.brand, sharePath: input.sharePath });
75
+ renderBrandStanza({ brand: input.brand, sharePath: input.sharePath, installOwner: input.installOwner });
73
76
  }
74
77
  // ---------------------------------------------------------------------------
75
78
  // LAN interface detection
@@ -148,9 +151,9 @@ function findSectionRange(conf, section) {
148
151
  * brand stanzas (other `[…]` blocks) are preserved verbatim.
149
152
  */
150
153
  export function mergeSmbConf(input) {
151
- const { existing, brand, sharePath, lanInterface } = input;
154
+ const { existing, brand, sharePath, lanInterface, installOwner } = input;
152
155
  const newGlobals = renderGlobalSection({ lanInterface });
153
- const newBrand = renderBrandStanza({ brand, sharePath });
156
+ const newBrand = renderBrandStanza({ brand, sharePath, installOwner });
154
157
  let conf = existing;
155
158
  // Replace or insert the global section.
156
159
  const globalRange = findSectionRange(conf, "global");
package/dist/uninstall.js CHANGED
@@ -759,9 +759,25 @@ function removeSamba() {
759
759
  catch (err) {
760
760
  console.log(` Failed to disable smbd/nmbd: ${err instanceof Error ? err.message : String(err)}`);
761
761
  }
762
- // Drop the admin smbpasswd entry. `smbpasswd -x` exits 0 on success, non-zero
763
- // if the user isn't in the passdb — both are acceptable end-states.
764
- spawnSync("sudo", ["smbpasswd", "-x", "admin"], { stdio: "pipe", timeout: 10_000 });
762
+ // Drop the install-owner's smbpasswd entry. `smbpasswd -x` exits 0 on
763
+ // success, non-zero if the user isn't in the passdb — both are acceptable
764
+ // end-states. Owner is read from the persisted file the installer wrote
765
+ // so the cleanup targets the same Unix user the provisioning step keyed
766
+ // on (Task 534): `admin` on a Pi/Hetzner box, `neo` (etc.) on a laptop.
767
+ // Missing file falls back to `admin` because an install that ran before
768
+ // the install-owner persistence existed only ever provisioned `admin`.
769
+ const installOwnerFile = join(CONFIG_DIR, ".install-owner");
770
+ let installOwner = "admin";
771
+ if (existsSync(installOwnerFile)) {
772
+ try {
773
+ const raw = readFileSync(installOwnerFile, "utf-8").trim();
774
+ if (raw.length > 0)
775
+ installOwner = raw;
776
+ }
777
+ catch { /* keep default */ }
778
+ }
779
+ spawnSync("sudo", ["smbpasswd", "-x", installOwner], { stdio: "pipe", timeout: 10_000 });
780
+ console.log(` smbpasswd -x ${installOwner}`);
765
781
  try {
766
782
  shell("apt-get", ["remove", "--purge", "-y", "samba", "samba-common-bin"], { sudo: true, timeout: 120_000 });
767
783
  console.log(" Purged samba package.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.194",
3
+ "version": "0.1.196",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -78,3 +78,7 @@ Tell {{productName}} to change which actions require approval:
78
78
  - "What actions currently require my approval?" — lists the current policy
79
79
 
80
80
  Changes are per-account and take effect immediately.
81
+
82
+ ## Filesystem Access (SMB Share)
83
+
84
+ Brand isolation extends to the device filesystem. Every {{productName}} install provisions an SMB share scoped to that brand's install folder, credentialled by the brand's install owner and the {{productName}} PIN. A device that hosts more than one brand carries one share per brand; tearing one brand down never exposes another brand's files. See [Samba Share](./samba.md) for the credential model, per-OS mount syntax, and peer-brand lifecycle.
@@ -121,25 +121,7 @@ If you need to restart the service manually (rare), ask {{productName}} to do it
121
121
 
122
122
  ## Browsing the brand filesystem on your LAN (SMB)
123
123
 
124
- {{productName}} exposes its install folder (`/home/admin/<brand>` on the Pi) as a network share so you can drop files in, drag files out, and edit them from your Mac, Windows PC, iPhone, or Android device. It uses SMB — the same protocol Windows file sharing uses — which every modern OS speaks natively. No client install required.
125
-
126
- The share is **LAN-only**: it binds to the loopback and your Pi's Wi-Fi/Ethernet interface only. Cloudflare tunnels carry HTTPS for the web UI, not SMB, so the share is invisible from the public internet. Off-LAN access for travelling operators is handled by a separate route (in progress).
127
-
128
- **Credentials:** SMB user is `admin`; password is your {{productName}} PIN. The installer wires the sync so every time you set or rotate the PIN in the admin UI, the SMB password updates with it.
129
-
130
- The SMB username is the Linux user that owns the install on the device — `admin` on a Pi or a Hetzner box, your own Linux user (for example `neo`) on a self-hosted laptop install. The password is your {{productName}} PIN. Changing the PIN in the admin UI re-syncs the SMB password for that same Unix user automatically.
131
-
132
- **macOS Finder**: Press `Cmd-K` from any Finder window, type `smb://<hostname>.local` (use the hostname your installer printed — for example `smb://maxy-code.local` or `smb://realagent-code.local`), click Connect, sign in as the install owner with your PIN. The share appears in the Finder sidebar; drag-and-drop works in both directions.
133
-
134
- **Windows Explorer**: Open File Explorer, type `\\<hostname>.local\<brand>` in the address bar (for example `\\maxy-code.local\maxy-code`), press Enter, sign in as the install owner with your PIN. To keep it across reboots, right-click → "Map network drive".
135
-
136
- **iOS Files**: Open Files → tap the `…` menu (top-right) → "Connect to Server" → enter `smb://<hostname>.local` → "Registered User" → username is the install owner, password is your PIN.
137
-
138
- **Android (Solid Explorer, CX File Explorer)**: Add a new connection of type SMB / Network. Host = `<hostname>.local`, share = `<brand>` (same as your install folder name), user = the install owner, password = your PIN.
139
-
140
- **Troubleshooting:** if the mount fails with "logon failure", change your PIN in the admin UI and try again — that re-triggers the smbpasswd sync. If the share doesn't show up at all, your client may need `<hostname>.local` resolved by mDNS — try the Pi's LAN IP address as a fallback (`smb://192.168.1.50` on macOS, `\\192.168.1.50\<brand>` on Windows).
141
-
142
- The installer maintains the share automatically. To remove it, uninstalling the brand strips its stanza from `/etc/samba/smb.conf` and (when no peer brand remains on the device) stops `smbd`, drops the smbpasswd entry, and purges the samba package.
124
+ Every install provisions a per-brand SMB share against the brand's install folder. See [Samba Share](./samba.md) for the share path, credentials, per-OS mount instructions, peer-brand lifecycle, and the LAN-only binding posture.
143
125
 
144
126
  ## Remote Access via Cloudflare
145
127
 
@@ -0,0 +1,80 @@
1
+ # Samba Share
2
+
3
+ Every {{productName}} install provisions a per-brand SMB network share so you can read and write the brand's install folder from Finder, File Explorer, the Files app on iOS, or any SMB-capable client on Android or Linux. No client install required — every modern OS speaks SMB natively.
4
+
5
+ The share lives next to the rest of the brand. On a device that runs more than one brand, each brand gets its own stanza, its own credentials, and its own lifecycle. Tearing one brand down never touches another brand's share.
6
+
7
+ ## What gets provisioned
8
+
9
+ The installer runs the same Samba step on every supported footprint — Raspberry Pi, Hetzner Cloud server, and self-hosted Linux laptop. Four sub-steps emit `[install-invariant] samba-provision-<step>` markers in order:
10
+
11
+ 1. **apt** — installs the `samba` package (skipped if `dpkg -s samba` already reports installed, so re-runs don't fight `unattended-upgrades` for the dpkg lock).
12
+ 2. **conf** — writes `/etc/samba/smb.conf` with a LAN-only `[global]` section plus a `[<brand>]` stanza pointing at the brand's install directory. The stanza is owned by the install owner (see below) and is marked `read only = no`, `browseable = yes`.
13
+ 3. **user** — deferred at install time on a fresh Pi or Hetzner box because there is no PIN to hand to `smbpasswd` yet. The user is created the moment the operator sets a PIN in the admin UI (see "PIN rotation" below).
14
+ 4. **units** — `systemctl enable --now smbd nmbd` so the share is reachable as soon as the install finishes.
15
+
16
+ macOS install is a no-op for this step — the installer logs `samba-provision skipped: platform=darwin` and returns. Mac operators do not get an SMB share against their laptop.
17
+
18
+ ## Share path
19
+
20
+ | Client | Address |
21
+ |---|---|
22
+ | macOS Finder | `smb://<hostname>.local` then pick the `<brand>` share |
23
+ | Windows Explorer | `\\<hostname>.local\<brand>` |
24
+ | Linux (`mount.cifs`, Nautilus, KDE) | `//<hostname>.local/<brand>` |
25
+ | iOS Files | `smb://<hostname>.local` |
26
+ | Android (Solid Explorer, CX File Explorer) | Host `<hostname>.local`, share `<brand>` |
27
+
28
+ `<hostname>` is whatever the installer printed at the end of `npx @rubytech/create-<brand>-code install` — usually the brand name on a fresh Pi (`maxy-code.local`, `realagent-code.local`). `<brand>` is the same string — it is also the install folder name under the install owner's home.
29
+
30
+ If `<hostname>.local` does not resolve from your client (some networks do not route mDNS), fall back to the LAN IP: `smb://192.168.1.50` on macOS, `\\192.168.1.50\<brand>` on Windows.
31
+
32
+ ## Credentials
33
+
34
+ - **Username** — the Unix user that owns the install on the device. On a Pi or Hetzner box this is `admin`; on a self-hosted Linux laptop it is whatever Linux user ran the installer (for example `neo`). The installer persists this value to `~/.<brand>/.install-owner` so every later read uses the same identity the installer wrote.
35
+ - **Password** — your current {{productName}} PIN. The same PIN that unlocks the admin UI unlocks the SMB share.
36
+
37
+ Both halves are required. SMB never accepts a guest connection; `map to guest = bad user` is set in the global stanza.
38
+
39
+ ## PIN rotation
40
+
41
+ There is no separate "SMB password." When you set or rotate the PIN in the admin UI, the platform's `set-pin` route runs `sudo -n smbpasswd -a -s <install-owner>` inline with the new PIN, behind a `NOPASSWD` sudoers grant written at install time and scoped to that exact command.
42
+
43
+ So:
44
+
45
+ - On a fresh Pi or Hetzner box, the share is reachable as soon as you set the first PIN. Before that point the `smbpasswd` entry does not exist and the mount fails with a logon error — that is expected.
46
+ - Rotating the PIN re-syncs the SMB password to the new value on the next set-pin request. Mounts using the old PIN start failing immediately; remount with the new PIN.
47
+ - If `set-pin` cannot read `~/.<brand>/.install-owner` (file missing or empty), it logs `[set-pin] smbpasswd sync failed owner=<unknown> rc=-1 reason=install-owner-file-missing` and skips the sync. The PIN still writes to the admin UI, but the SMB mount keeps refusing the new password until the install-owner file is restored.
48
+
49
+ ## LAN-only binding
50
+
51
+ The `[global]` section binds smbd to loopback plus one LAN interface:
52
+
53
+ ```
54
+ interfaces = lo <lan>
55
+ bind interfaces only = yes
56
+ ```
57
+
58
+ `<lan>` is whichever non-loopback interface has an IPv4 address — `wlan0` preferred, then `eth0`, then the first other interface with an address. If the device has no LAN interface at all, the installer refuses to provision and exits — there is nothing safe to bind to.
59
+
60
+ This is the structural guarantee that SMB never leaves the LAN, even if upstream firewall rules are misconfigured. The Cloudflare tunnel that fronts the admin UI carries HTTPS only; it does not route SMB. **On a Hetzner box the share is therefore not reachable from the public internet** — operators reach it by `ssh -L 4445:localhost:445 admin@<tunnel-host>` and then mounting `smb://localhost:4445`, or by running the Hetzner box on a private network that the operator's machine also joins.
61
+
62
+ ## Peer-brand lifecycle
63
+
64
+ A device that hosts more than one brand carries one stanza per brand in `/etc/samba/smb.conf`. The provisioner is idempotent and peer-safe:
65
+
66
+ - **Install a second brand** — the new brand's stanza is appended next to the existing one. The shared `[global]` section is rewritten to keep the LAN-only directives current but is otherwise unchanged. Peer-brand stanzas are preserved byte-for-byte.
67
+ - **Re-run the installer on an existing brand** — the brand's own stanza is replaced in place. Peer stanzas are not touched.
68
+ - **Uninstall one brand** — only that brand's stanza is stripped. `smbd` is then `reload`ed so the brand share disappears from the running config without dropping connections to peer shares.
69
+ - **Uninstall the last brand** — after the stanza is removed, the uninstaller checks `hasAnyBrandStanza()` and, if false, stops and disables `smbd`/`nmbd`, runs `smbpasswd -x <install-owner>` to drop the smbpasswd entry, and `apt-purge samba`. If any peer stanza remains, the units stay running and the package stays installed — the uninstaller logs `Leaving smbd/nmbd + samba package in place — other brand stanza remains`.
70
+
71
+ The brand-stanza name is the only identifier the uninstaller matches on, so two brands with different `BRAND.hostname` values cannot collide.
72
+
73
+ ## Troubleshooting
74
+
75
+ - **"Logon failure" on mount.** The PIN you typed does not match the current `smbpasswd` entry. Set a new PIN in the admin UI and remount. If the PIN was just rotated and the mount still fails, check `~/.<brand>/.install-owner` exists and is non-empty.
76
+ - **Share does not show up in Finder / network browser.** mDNS may not be routed on your network. Mount by LAN IP instead of `<hostname>.local`.
77
+ - **`smbd` not running after install.** Check the install log for the four `[install-invariant] samba-provision-<step>` markers. The `units` step running `systemctl enable --now smbd nmbd` is the last to fire; if it failed the marker prints `fail: <reason>`.
78
+ - **Hetzner share not reachable from outside the box.** This is by design — see "LAN-only binding" above. Use SSH port forwarding.
79
+
80
+ Also see [Deployment Guide](./deployment.md) for the surrounding install flow, and [Access Control](./access-control.md) for how the brand isolation extends from the admin UI to the SMB share.
@@ -134,6 +134,20 @@ tail -200 ~/.maxy/logs/maxy-ui.log | rg '\[remote-auth\].*resolvedKind='
134
134
 
135
135
  ---
136
136
 
137
+ ## Cannot Mount the SMB Share
138
+
139
+ **Symptom:** Mounting `smb://<hostname>.local` (or `\\<hostname>.local\<brand>`) fails with a "logon failure" or the share does not appear in your network browser.
140
+
141
+ **Check:**
142
+ 1. Confirm you have set a PIN in the admin UI at least once. On a fresh Pi or Hetzner box the `smbpasswd` entry does not exist until the first set-pin runs — mounts before that point always fail.
143
+ 2. Use the install owner as the username (`admin` on a Pi or Hetzner box; the Linux user that ran the installer on a self-hosted laptop) and the current {{productName}} PIN as the password. The SMB password is not stored separately — it is the PIN.
144
+ 3. If `<hostname>.local` does not resolve from your client, mount by LAN IP instead (`smb://192.168.1.50` on macOS, `\\192.168.1.50\<brand>` on Windows).
145
+ 4. Rotate the PIN in the admin UI. That re-triggers the `smbpasswd` sync on the device. If the resync log line reads `[set-pin] smbpasswd sync failed owner=<unknown> rc=-1 reason=install-owner-file-missing`, restore `~/.<brand>/.install-owner` from the installer log.
146
+
147
+ See [Samba Share](./samba.md) for the full credential model and per-OS mount syntax.
148
+
149
+ ---
150
+
137
151
  ## Restarting the Platform
138
152
 
139
153
  From the admin interface, ask {{productName}}: "Restart the platform."
@@ -4,9 +4,6 @@ import type { FsWatcher } from './fs-watcher.js';
4
4
  import type { RateLimiter } from './spawn-rate-limiter.js';
5
5
  import type { AuditRegistry } from './public-tool-audit.js';
6
6
  import { type UserTitleStore } from './user-title-store.js';
7
- /** Test-only — clear the dedup cache between vitest cases so module state
8
- * does not leak across describe blocks. */
9
- export declare function __resetResumeDedupCacheForTests(): void;
10
7
  export declare const SESSION_ID_PATTERN: RegExp;
11
8
  export interface HttpDeps extends Omit<SpawnDeps, 'onSessionReady' | 'watcher'> {
12
9
  watcher: FsWatcher;
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,EAcL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAYzB,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,iBAAiB,CAAA;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAwB9E;4CAC4C;AAC5C,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD;AAwED,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAI1D,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;CAC/B;AA4LD;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAo5CpD"}
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAI3B,OAAO,EAcL,KAAK,SAAS,EAEf,MAAM,kBAAkB,CAAA;AAYzB,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,iBAAiB,CAAA;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAC3D,OAAO,EAAqB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAA;AA+E9E,eAAO,MAAM,kBAAkB,QAA2B,CAAA;AAI1D,MAAM,WAAW,QAAS,SAAQ,IAAI,CAAC,SAAS,EAAE,gBAAgB,GAAG,SAAS,CAAC;IAC7E,OAAO,EAAE,SAAS,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,WAAW,CAAA;IAC/B,eAAe,EAAE,aAAa,CAAA;IAC9B;kFAC8E;IAC9E,cAAc,EAAE,cAAc,CAAA;CAC/B;AA4LD;;;kEAGkE;AAClE,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;+CAG2C;IAC3C,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD,CAAA;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CA+oCpD"}