@knowzai/cli 0.3.0 → 0.5.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 (289) hide show
  1. package/README.md +68 -0
  2. package/assets/portable/releases.json +42 -0
  3. package/assets/selfhosted/.env.example +22 -1
  4. package/assets/selfhosted/docker-compose.mssql.yml +113 -0
  5. package/assets/selfhosted/docker-compose.yml +29 -15
  6. package/assets/selfhosted/lmstudio-azure-bridge.mjs +73 -0
  7. package/assets/selfhosted/releases.json +129 -6
  8. package/bin/run.js +2 -0
  9. package/dist/.tsbuildinfo +1 -1
  10. package/dist/base-command.d.ts +2 -0
  11. package/dist/base-command.d.ts.map +1 -1
  12. package/dist/base-command.js +11 -1
  13. package/dist/base-command.js.map +1 -1
  14. package/dist/commands/ask.d.ts.map +1 -1
  15. package/dist/commands/ask.js +2 -0
  16. package/dist/commands/ask.js.map +1 -1
  17. package/dist/commands/backup.d.ts +1 -0
  18. package/dist/commands/backup.d.ts.map +1 -1
  19. package/dist/commands/backup.js +29 -10
  20. package/dist/commands/backup.js.map +1 -1
  21. package/dist/commands/chat.d.ts +2 -1
  22. package/dist/commands/chat.d.ts.map +1 -1
  23. package/dist/commands/chat.js +10 -4
  24. package/dist/commands/chat.js.map +1 -1
  25. package/dist/commands/down.d.ts +14 -27
  26. package/dist/commands/down.d.ts.map +1 -1
  27. package/dist/commands/down.js +6 -31
  28. package/dist/commands/down.js.map +1 -1
  29. package/dist/commands/knowledge/amend.d.ts +2 -2
  30. package/dist/commands/knowledge/amend.d.ts.map +1 -1
  31. package/dist/commands/knowledge/amend.js +5 -0
  32. package/dist/commands/knowledge/amend.js.map +1 -1
  33. package/dist/commands/login.d.ts +4 -0
  34. package/dist/commands/login.d.ts.map +1 -1
  35. package/dist/commands/login.js +21 -2
  36. package/dist/commands/login.js.map +1 -1
  37. package/dist/commands/menu.js +1 -1
  38. package/dist/commands/menu.js.map +1 -1
  39. package/dist/commands/platform/destroy.d.ts +15 -5
  40. package/dist/commands/platform/destroy.d.ts.map +1 -1
  41. package/dist/commands/platform/destroy.js +6 -33
  42. package/dist/commands/platform/destroy.js.map +1 -1
  43. package/dist/commands/platform/doctor.d.ts +18 -3
  44. package/dist/commands/platform/doctor.d.ts.map +1 -1
  45. package/dist/commands/platform/doctor.js +4 -10
  46. package/dist/commands/platform/doctor.js.map +1 -1
  47. package/dist/commands/platform/down.d.ts +15 -15
  48. package/dist/commands/platform/down.d.ts.map +1 -1
  49. package/dist/commands/platform/down.js +4 -8
  50. package/dist/commands/platform/down.js.map +1 -1
  51. package/dist/commands/platform/logs.d.ts +15 -14
  52. package/dist/commands/platform/logs.d.ts.map +1 -1
  53. package/dist/commands/platform/logs.js +5 -16
  54. package/dist/commands/platform/logs.js.map +1 -1
  55. package/dist/commands/platform/reset.d.ts +16 -13
  56. package/dist/commands/platform/reset.d.ts.map +1 -1
  57. package/dist/commands/platform/reset.js +7 -36
  58. package/dist/commands/platform/reset.js.map +1 -1
  59. package/dist/commands/platform/setup.d.ts +1 -0
  60. package/dist/commands/platform/setup.d.ts.map +1 -1
  61. package/dist/commands/platform/setup.js +1 -1
  62. package/dist/commands/platform/setup.js.map +1 -1
  63. package/dist/commands/platform/status.d.ts +14 -16
  64. package/dist/commands/platform/status.d.ts.map +1 -1
  65. package/dist/commands/platform/status.js +4 -29
  66. package/dist/commands/platform/status.js.map +1 -1
  67. package/dist/commands/restore.d.ts +10 -1
  68. package/dist/commands/restore.d.ts.map +1 -1
  69. package/dist/commands/restore.js +56 -13
  70. package/dist/commands/restore.js.map +1 -1
  71. package/dist/commands/runtime/destroy.d.ts +29 -0
  72. package/dist/commands/runtime/destroy.d.ts.map +1 -0
  73. package/dist/commands/runtime/destroy.js +13 -0
  74. package/dist/commands/runtime/destroy.js.map +1 -0
  75. package/dist/commands/runtime/doctor.d.ts +27 -0
  76. package/dist/commands/runtime/doctor.d.ts.map +1 -0
  77. package/dist/commands/runtime/doctor.js +11 -0
  78. package/dist/commands/runtime/doctor.js.map +1 -0
  79. package/dist/commands/runtime/logs.d.ts +28 -0
  80. package/dist/commands/runtime/logs.d.ts.map +1 -0
  81. package/dist/commands/runtime/logs.js +12 -0
  82. package/dist/commands/runtime/logs.js.map +1 -0
  83. package/dist/commands/runtime/status.d.ts +27 -0
  84. package/dist/commands/runtime/status.d.ts.map +1 -0
  85. package/dist/commands/runtime/status.js +11 -0
  86. package/dist/commands/runtime/status.js.map +1 -0
  87. package/dist/commands/runtime/upgrade.d.ts +28 -0
  88. package/dist/commands/runtime/upgrade.d.ts.map +1 -0
  89. package/dist/commands/runtime/upgrade.js +12 -0
  90. package/dist/commands/runtime/upgrade.js.map +1 -0
  91. package/dist/commands/setup.d.ts +1 -0
  92. package/dist/commands/setup.d.ts.map +1 -1
  93. package/dist/commands/setup.js +1 -1
  94. package/dist/commands/transfer.js +1 -1
  95. package/dist/commands/transfer.js.map +1 -1
  96. package/dist/commands/up.d.ts +3 -11
  97. package/dist/commands/up.d.ts.map +1 -1
  98. package/dist/commands/up.js +48 -36
  99. package/dist/commands/up.js.map +1 -1
  100. package/dist/commands/whoami.js +1 -1
  101. package/dist/commands/whoami.js.map +1 -1
  102. package/dist/hooks/init/launch-menu.d.ts.map +1 -1
  103. package/dist/hooks/init/launch-menu.js +2 -0
  104. package/dist/hooks/init/launch-menu.js.map +1 -1
  105. package/dist/lib/anonymous-registry.d.ts +3 -0
  106. package/dist/lib/anonymous-registry.d.ts.map +1 -0
  107. package/dist/lib/anonymous-registry.js +25 -0
  108. package/dist/lib/anonymous-registry.js.map +1 -0
  109. package/dist/lib/async-process.d.ts +15 -0
  110. package/dist/lib/async-process.d.ts.map +1 -0
  111. package/dist/lib/async-process.js +145 -0
  112. package/dist/lib/async-process.js.map +1 -0
  113. package/dist/lib/backup-process.d.ts +4 -0
  114. package/dist/lib/backup-process.d.ts.map +1 -0
  115. package/dist/lib/backup-process.js +20 -0
  116. package/dist/lib/backup-process.js.map +1 -0
  117. package/dist/lib/brand.d.ts +25 -0
  118. package/dist/lib/brand.d.ts.map +1 -0
  119. package/dist/lib/brand.js +105 -0
  120. package/dist/lib/brand.js.map +1 -0
  121. package/dist/lib/chat.d.ts +1 -0
  122. package/dist/lib/chat.d.ts.map +1 -1
  123. package/dist/lib/chat.js +12 -3
  124. package/dist/lib/chat.js.map +1 -1
  125. package/dist/lib/compose-status.d.ts +14 -0
  126. package/dist/lib/compose-status.d.ts.map +1 -0
  127. package/dist/lib/compose-status.js +31 -0
  128. package/dist/lib/compose-status.js.map +1 -0
  129. package/dist/lib/container-engine.js +2 -2
  130. package/dist/lib/container-engine.js.map +1 -1
  131. package/dist/lib/local-engine-context.d.ts +16 -0
  132. package/dist/lib/local-engine-context.d.ts.map +1 -0
  133. package/dist/lib/local-engine-context.js +123 -0
  134. package/dist/lib/local-engine-context.js.map +1 -0
  135. package/dist/lib/login-guided.d.ts.map +1 -1
  136. package/dist/lib/login-guided.js +2 -1
  137. package/dist/lib/login-guided.js.map +1 -1
  138. package/dist/lib/login.d.ts +3 -0
  139. package/dist/lib/login.d.ts.map +1 -1
  140. package/dist/lib/login.js +25 -5
  141. package/dist/lib/login.js.map +1 -1
  142. package/dist/lib/oci-registry-probe.d.ts +43 -0
  143. package/dist/lib/oci-registry-probe.d.ts.map +1 -0
  144. package/dist/lib/oci-registry-probe.js +110 -0
  145. package/dist/lib/oci-registry-probe.js.map +1 -0
  146. package/dist/lib/peer-command.d.ts.map +1 -1
  147. package/dist/lib/peer-command.js +3 -1
  148. package/dist/lib/peer-command.js.map +1 -1
  149. package/dist/lib/platform-acquisition.d.ts +27 -0
  150. package/dist/lib/platform-acquisition.d.ts.map +1 -0
  151. package/dist/lib/platform-acquisition.js +21 -0
  152. package/dist/lib/platform-acquisition.js.map +1 -0
  153. package/dist/lib/platform-backup.d.ts +1 -0
  154. package/dist/lib/platform-backup.d.ts.map +1 -1
  155. package/dist/lib/platform-backup.js +375 -157
  156. package/dist/lib/platform-backup.js.map +1 -1
  157. package/dist/lib/platform-command-shared.d.ts +2 -0
  158. package/dist/lib/platform-command-shared.d.ts.map +1 -1
  159. package/dist/lib/platform-command-shared.js +13 -5
  160. package/dist/lib/platform-command-shared.js.map +1 -1
  161. package/dist/lib/platform-edition.d.ts +3 -6
  162. package/dist/lib/platform-edition.d.ts.map +1 -1
  163. package/dist/lib/platform-edition.js +9 -17
  164. package/dist/lib/platform-edition.js.map +1 -1
  165. package/dist/lib/platform-frontdoor-runtime.d.ts.map +1 -1
  166. package/dist/lib/platform-frontdoor-runtime.js +44 -19
  167. package/dist/lib/platform-frontdoor-runtime.js.map +1 -1
  168. package/dist/lib/platform-frontdoor.d.ts +32 -5
  169. package/dist/lib/platform-frontdoor.d.ts.map +1 -1
  170. package/dist/lib/platform-frontdoor.js +251 -171
  171. package/dist/lib/platform-frontdoor.js.map +1 -1
  172. package/dist/lib/platform-image-admission.d.ts +16 -0
  173. package/dist/lib/platform-image-admission.d.ts.map +1 -0
  174. package/dist/lib/platform-image-admission.js +73 -0
  175. package/dist/lib/platform-image-admission.js.map +1 -0
  176. package/dist/lib/platform-image-source.d.ts.map +1 -1
  177. package/dist/lib/platform-image-source.js +2 -0
  178. package/dist/lib/platform-image-source.js.map +1 -1
  179. package/dist/lib/platform-lifecycle.d.ts +4 -0
  180. package/dist/lib/platform-lifecycle.d.ts.map +1 -1
  181. package/dist/lib/platform-lifecycle.js +42 -8
  182. package/dist/lib/platform-lifecycle.js.map +1 -1
  183. package/dist/lib/platform-onboarding.d.ts +10 -0
  184. package/dist/lib/platform-onboarding.d.ts.map +1 -1
  185. package/dist/lib/platform-onboarding.js +21 -0
  186. package/dist/lib/platform-onboarding.js.map +1 -1
  187. package/dist/lib/platform-preflight.d.ts +6 -3
  188. package/dist/lib/platform-preflight.d.ts.map +1 -1
  189. package/dist/lib/platform-preflight.js +52 -19
  190. package/dist/lib/platform-preflight.js.map +1 -1
  191. package/dist/lib/platform-release-manifest.d.ts +6 -1
  192. package/dist/lib/platform-release-manifest.d.ts.map +1 -1
  193. package/dist/lib/platform-release-manifest.js +10 -0
  194. package/dist/lib/platform-release-manifest.js.map +1 -1
  195. package/dist/lib/platform-runtime.d.ts +5 -2
  196. package/dist/lib/platform-runtime.d.ts.map +1 -1
  197. package/dist/lib/platform-runtime.js +7 -40
  198. package/dist/lib/platform-runtime.js.map +1 -1
  199. package/dist/lib/portable-release-digests.d.ts +5 -0
  200. package/dist/lib/portable-release-digests.d.ts.map +1 -0
  201. package/dist/lib/portable-release-digests.js +53 -0
  202. package/dist/lib/portable-release-digests.js.map +1 -0
  203. package/dist/lib/release-pin-matrix.d.ts +29 -0
  204. package/dist/lib/release-pin-matrix.d.ts.map +1 -0
  205. package/dist/lib/release-pin-matrix.js +67 -0
  206. package/dist/lib/release-pin-matrix.js.map +1 -0
  207. package/dist/lib/runtime-backup-archive.d.ts +20 -0
  208. package/dist/lib/runtime-backup-archive.d.ts.map +1 -0
  209. package/dist/lib/runtime-backup-archive.js +269 -0
  210. package/dist/lib/runtime-backup-archive.js.map +1 -0
  211. package/dist/lib/runtime-command.d.ts +37 -0
  212. package/dist/lib/runtime-command.d.ts.map +1 -0
  213. package/dist/lib/runtime-command.js +50 -0
  214. package/dist/lib/runtime-command.js.map +1 -0
  215. package/dist/lib/runtime-discovery.d.ts +18 -0
  216. package/dist/lib/runtime-discovery.d.ts.map +1 -0
  217. package/dist/lib/runtime-discovery.js +56 -0
  218. package/dist/lib/runtime-discovery.js.map +1 -0
  219. package/dist/lib/runtime-lock.d.ts +3 -0
  220. package/dist/lib/runtime-lock.d.ts.map +1 -0
  221. package/dist/lib/runtime-lock.js +50 -0
  222. package/dist/lib/runtime-lock.js.map +1 -0
  223. package/dist/lib/runtime-operations.d.ts +98 -0
  224. package/dist/lib/runtime-operations.d.ts.map +1 -0
  225. package/dist/lib/runtime-operations.js +182 -0
  226. package/dist/lib/runtime-operations.js.map +1 -0
  227. package/dist/lib/runtime-provider.d.ts +17 -0
  228. package/dist/lib/runtime-provider.d.ts.map +1 -0
  229. package/dist/lib/runtime-provider.js +52 -0
  230. package/dist/lib/runtime-provider.js.map +1 -0
  231. package/dist/lib/runtime-status.d.ts +24 -0
  232. package/dist/lib/runtime-status.d.ts.map +1 -0
  233. package/dist/lib/runtime-status.js +128 -0
  234. package/dist/lib/runtime-status.js.map +1 -0
  235. package/dist/lib/selfhosted-backup.d.ts +37 -0
  236. package/dist/lib/selfhosted-backup.d.ts.map +1 -0
  237. package/dist/lib/selfhosted-backup.js +446 -0
  238. package/dist/lib/selfhosted-backup.js.map +1 -0
  239. package/dist/lib/selfhosted-keyring.d.ts +19 -0
  240. package/dist/lib/selfhosted-keyring.d.ts.map +1 -0
  241. package/dist/lib/selfhosted-keyring.js +193 -0
  242. package/dist/lib/selfhosted-keyring.js.map +1 -0
  243. package/dist/lib/selfhosted-lifecycle.d.ts +66 -10
  244. package/dist/lib/selfhosted-lifecycle.d.ts.map +1 -1
  245. package/dist/lib/selfhosted-lifecycle.js +344 -39
  246. package/dist/lib/selfhosted-lifecycle.js.map +1 -1
  247. package/dist/lib/selfhosted-login.d.ts +34 -0
  248. package/dist/lib/selfhosted-login.d.ts.map +1 -0
  249. package/dist/lib/selfhosted-login.js +116 -0
  250. package/dist/lib/selfhosted-login.js.map +1 -0
  251. package/dist/lib/selfhosted-readiness.d.ts +3 -0
  252. package/dist/lib/selfhosted-readiness.d.ts.map +1 -0
  253. package/dist/lib/selfhosted-readiness.js +24 -0
  254. package/dist/lib/selfhosted-readiness.js.map +1 -0
  255. package/dist/lib/setup-capabilities.d.ts +15 -0
  256. package/dist/lib/setup-capabilities.d.ts.map +1 -0
  257. package/dist/lib/setup-capabilities.js +21 -0
  258. package/dist/lib/setup-capabilities.js.map +1 -0
  259. package/dist/lib/setup-console.d.ts +11 -0
  260. package/dist/lib/setup-console.d.ts.map +1 -1
  261. package/dist/lib/setup-console.js +136 -74
  262. package/dist/lib/setup-console.js.map +1 -1
  263. package/dist/lib/setup-page.d.ts +12 -0
  264. package/dist/lib/setup-page.d.ts.map +1 -1
  265. package/dist/lib/setup-page.js +56 -27
  266. package/dist/lib/setup-page.js.map +1 -1
  267. package/dist/lib/setup-plan.d.ts +1 -0
  268. package/dist/lib/setup-plan.d.ts.map +1 -1
  269. package/dist/lib/setup-plan.js +5 -1
  270. package/dist/lib/setup-plan.js.map +1 -1
  271. package/dist/lib/setup-server.d.ts +11 -2
  272. package/dist/lib/setup-server.d.ts.map +1 -1
  273. package/dist/lib/setup-server.js +150 -26
  274. package/dist/lib/setup-server.js.map +1 -1
  275. package/dist/lib/ui.d.ts +3 -1
  276. package/dist/lib/ui.d.ts.map +1 -1
  277. package/dist/lib/ui.js +39 -7
  278. package/dist/lib/ui.js.map +1 -1
  279. package/dist/lib/up-result.d.ts +3 -0
  280. package/dist/lib/up-result.d.ts.map +1 -1
  281. package/dist/lib/up-result.js.map +1 -1
  282. package/dist/menu/run.js +1 -1
  283. package/dist/menu/run.js.map +1 -1
  284. package/dist/menu/should-launch.d.ts +0 -14
  285. package/dist/menu/should-launch.d.ts.map +1 -1
  286. package/dist/menu/should-launch.js +5 -1
  287. package/dist/menu/should-launch.js.map +1 -1
  288. package/oclif.manifest.json +5052 -2825
  289. package/package.json +12 -9
@@ -1,10 +1,12 @@
1
- import { chmodSync, closeSync, copyFileSync, cpSync, createReadStream, createWriteStream, existsSync, fstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
1
+ import { assertLocalEngineContext, readSavedLocalEngine } from './local-engine-context.js';
2
+ import { acquireRuntimeLock } from './runtime-lock.js';
3
+ import { closeSync, copyFileSync, cpSync, createReadStream, createWriteStream, existsSync, fstatSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
4
  import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync } from 'node:crypto';
3
5
  import { dirname, isAbsolute, join, resolve } from 'node:path';
4
6
  import { tmpdir } from 'node:os';
5
7
  import { fileURLToPath } from 'node:url';
6
8
  import { pipeline } from 'node:stream/promises';
7
- import { ExitCode, KnowzError } from '@knowzai/core';
9
+ import { ExitCode, KnowzError, createPrivateTempDirectory, ensurePrivateDirectory, protectPrivatePath, publishPrivateFile, withPrivateOutput } from '@knowzai/core';
8
10
  import { redactPlatformSecrets, parseEnv, writePrivateFile, } from './platform-runtime.js';
9
11
  export const BACKUP_SECURITY_WARNING = 'SECURITY WARNING: this backup contains the kzin_ installation credential, all tenant PII, legacy plaintext API-key columns, and every stack secret (.env, JWT, MinIO root password, and SuperAdmin passphrase). Treat it like a production credentials file; use --encrypt and store it securely.';
10
12
  export const BACKUP_SCRYPT = { N: 1 << 15, r: 8, p: 1, maxmem: 96 * 1024 * 1024 };
@@ -32,9 +34,12 @@ function composeInvocation(paths, projectName, engine, action) {
32
34
  command: front.command,
33
35
  args: [
34
36
  ...front.baseArgs,
35
- '--project-name', projectName,
36
- '--file', paths.composeFile,
37
- '--env-file', paths.envFile,
37
+ '--project-name',
38
+ projectName,
39
+ '--file',
40
+ paths.composeFile,
41
+ '--env-file',
42
+ paths.envFile,
38
43
  ...action,
39
44
  ],
40
45
  options: {
@@ -69,42 +74,82 @@ export function planBackup(paths, projectName, env, opts) {
69
74
  const steps = [];
70
75
  if (!opts.postgresRunning) {
71
76
  steps.push({
72
- kind: 'compose', description: 'start postgres',
73
- invocation: composeInvocation(paths, projectName, engine, ['up', '--detach', '--wait', 'postgres']),
77
+ kind: 'compose',
78
+ description: 'start postgres',
79
+ invocation: composeInvocation(paths, projectName, engine, [
80
+ 'up',
81
+ '--detach',
82
+ '--wait',
83
+ 'postgres',
84
+ ]),
74
85
  });
75
86
  }
76
87
  for (const database of ['KnowzMaster', 'KnowzKnowledge']) {
77
88
  steps.push({
78
- kind: 'compose', description: `dump ${database}`,
89
+ kind: 'compose',
90
+ description: `dump ${database}`,
79
91
  invocation: composeInvocation(paths, projectName, engine, [
80
- 'exec', '-T', 'postgres', 'pg_dump', '-U', pgUser, '-Fc', '-f', `/tmp/${database}.dump`, database,
92
+ 'exec',
93
+ '-T',
94
+ 'postgres',
95
+ 'pg_dump',
96
+ '-U',
97
+ pgUser,
98
+ '-Fc',
99
+ '-f',
100
+ `/tmp/${database}.dump`,
101
+ database,
81
102
  ]),
82
103
  });
83
104
  steps.push({
84
- kind: 'compose', description: `copy ${database} dump`,
105
+ kind: 'compose',
106
+ description: `copy ${database} dump`,
85
107
  invocation: composeInvocation(paths, projectName, engine, [
86
- 'cp', `postgres:/tmp/${database}.dump`, join(staging, 'pg', `${database}.dump`),
108
+ 'cp',
109
+ `postgres:/tmp/${database}.dump`,
110
+ join(staging, 'pg', `${database}.dump`),
87
111
  ]),
88
112
  });
89
113
  steps.push({
90
- kind: 'compose', description: `remove staged ${database} dump`,
91
- invocation: composeInvocation(paths, projectName, engine, ['exec', '-T', 'postgres', 'rm', '-f', `/tmp/${database}.dump`]),
114
+ kind: 'compose',
115
+ description: `remove staged ${database} dump`,
116
+ invocation: composeInvocation(paths, projectName, engine, [
117
+ 'exec',
118
+ '-T',
119
+ 'postgres',
120
+ 'rm',
121
+ '-f',
122
+ `/tmp/${database}.dump`,
123
+ ]),
92
124
  });
93
125
  }
94
126
  for (const volume of ['miniodata', 'objectsdata', 'identity']) {
95
127
  const physical = `${projectName}_knowz-portable-${volume}`;
96
128
  steps.push({
97
- kind: 'engine-run', description: `snapshot ${volume}`,
129
+ kind: 'engine-run',
130
+ description: `snapshot ${volume}`,
98
131
  invocation: engineInvocation(engine, paths.runtimeDir, [
99
- 'run', '--rm', '-v', `${physical}:/src:ro`, '-v', `${staging}:/out`,
100
- 'alpine', 'tar', '-czf', `/out/volumes/${volume}.tar.gz`, '-C', '/src', '.',
132
+ 'run',
133
+ '--rm',
134
+ '-v',
135
+ `${physical}:/src:ro`,
136
+ '-v',
137
+ `${staging}:/out`,
138
+ 'alpine',
139
+ 'tar',
140
+ '-czf',
141
+ `/out/volumes/${volume}.tar.gz`,
142
+ '-C',
143
+ '/src',
144
+ '.',
101
145
  ]),
102
146
  });
103
147
  }
104
148
  steps.push({ kind: 'fs', description: 'write manifest' });
105
149
  if (!opts.postgresRunning) {
106
150
  steps.push({
107
- kind: 'compose', description: 'stop postgres',
151
+ kind: 'compose',
152
+ description: 'stop postgres',
108
153
  invocation: composeInvocation(paths, projectName, engine, ['stop', 'postgres']),
109
154
  });
110
155
  }
@@ -118,41 +163,105 @@ export function planRestore(paths, projectName, manifest, opts) {
118
163
  steps.push({ kind: 'fs', description: 'restore runtime configuration and portable assets' });
119
164
  for (const volume of manifest.volumes) {
120
165
  const physical = `${projectName}_knowz-portable-${volume}`;
121
- steps.push({ kind: 'engine-run', description: `wipe ${physical}`, invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'rm', '--force', physical]) });
122
- steps.push({ kind: 'engine-run', description: `create ${physical}`, invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'create', physical]) });
166
+ steps.push({
167
+ kind: 'engine-run',
168
+ description: `wipe ${physical}`,
169
+ invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'rm', '--force', physical]),
170
+ });
171
+ steps.push({
172
+ kind: 'engine-run',
173
+ description: `create ${physical}`,
174
+ invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'create', physical]),
175
+ });
123
176
  if (volume !== 'identity' || opts.mode === 'replace-installation') {
124
177
  steps.push({
125
- kind: 'engine-run', description: `extract volumes/${volume}.tar.gz`,
178
+ kind: 'engine-run',
179
+ description: `extract volumes/${volume}.tar.gz`,
126
180
  invocation: engineInvocation(engine, paths.runtimeDir, [
127
- 'run', '--rm', '-v', `${physical}:/dst`, '-v', `${staging}:/in:ro`,
128
- 'alpine', 'tar', '-xzf', `/in/volumes/${volume}.tar.gz`, '-C', '/dst',
181
+ 'run',
182
+ '--rm',
183
+ '-v',
184
+ `${physical}:/dst`,
185
+ '-v',
186
+ `${staging}:/in:ro`,
187
+ 'alpine',
188
+ 'tar',
189
+ '-xzf',
190
+ `/in/volumes/${volume}.tar.gz`,
191
+ '-C',
192
+ '/dst',
129
193
  ]),
130
194
  });
131
195
  }
132
196
  }
133
197
  const pgVolume = `${projectName}_knowz-portable-pgdata`;
134
- steps.push({ kind: 'engine-run', description: `wipe ${pgVolume}`, invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'rm', '--force', pgVolume]) });
135
- steps.push({ kind: 'engine-run', description: `create ${pgVolume}`, invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'create', pgVolume]) });
136
- steps.push({ kind: 'compose', description: 'start postgres', invocation: composeInvocation(paths, projectName, engine, ['up', '--detach', '--wait', 'postgres']) });
198
+ steps.push({
199
+ kind: 'engine-run',
200
+ description: `wipe ${pgVolume}`,
201
+ invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'rm', '--force', pgVolume]),
202
+ });
203
+ steps.push({
204
+ kind: 'engine-run',
205
+ description: `create ${pgVolume}`,
206
+ invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'create', pgVolume]),
207
+ });
208
+ steps.push({
209
+ kind: 'compose',
210
+ description: 'start postgres',
211
+ invocation: composeInvocation(paths, projectName, engine, [
212
+ 'up',
213
+ '--detach',
214
+ '--wait',
215
+ 'postgres',
216
+ ]),
217
+ });
137
218
  const pgUser = opts.pgUser || 'knowz';
138
219
  for (const database of manifest.pgDatabases) {
139
220
  steps.push({
140
- kind: 'compose', description: `copy ${database} dump`,
141
- invocation: composeInvocation(paths, projectName, engine, ['cp', join(staging, 'pg', `${database}.dump`), `postgres:/tmp/${database}.dump`]),
221
+ kind: 'compose',
222
+ description: `copy ${database} dump`,
223
+ invocation: composeInvocation(paths, projectName, engine, [
224
+ 'cp',
225
+ join(staging, 'pg', `${database}.dump`),
226
+ `postgres:/tmp/${database}.dump`,
227
+ ]),
142
228
  });
143
229
  steps.push({
144
- kind: 'compose', description: `restore ${database}`,
230
+ kind: 'compose',
231
+ description: `restore ${database}`,
145
232
  invocation: composeInvocation(paths, projectName, engine, [
146
- 'exec', '-T', 'postgres', 'pg_restore', '--clean', '--if-exists', '--no-owner',
147
- '-U', pgUser, '-d', database, `/tmp/${database}.dump`,
233
+ 'exec',
234
+ '-T',
235
+ 'postgres',
236
+ 'pg_restore',
237
+ '--clean',
238
+ '--if-exists',
239
+ '--no-owner',
240
+ '-U',
241
+ pgUser,
242
+ '-d',
243
+ database,
244
+ `/tmp/${database}.dump`,
148
245
  ]),
149
246
  });
150
247
  steps.push({
151
- kind: 'compose', description: `remove staged ${database} dump`,
152
- invocation: composeInvocation(paths, projectName, engine, ['exec', '-T', 'postgres', 'rm', '-f', `/tmp/${database}.dump`]),
248
+ kind: 'compose',
249
+ description: `remove staged ${database} dump`,
250
+ invocation: composeInvocation(paths, projectName, engine, [
251
+ 'exec',
252
+ '-T',
253
+ 'postgres',
254
+ 'rm',
255
+ '-f',
256
+ `/tmp/${database}.dump`,
257
+ ]),
153
258
  });
154
259
  }
155
- steps.push({ kind: 'compose', description: 'stop postgres', invocation: composeInvocation(paths, projectName, engine, ['stop', 'postgres']) });
260
+ steps.push({
261
+ kind: 'compose',
262
+ description: 'stop postgres',
263
+ invocation: composeInvocation(paths, projectName, engine, ['stop', 'postgres']),
264
+ });
156
265
  return steps;
157
266
  }
158
267
  function fileSha256(path) {
@@ -206,13 +315,16 @@ export function parseAndValidateManifest(json) {
206
315
  if (!isRecord(value) || value.schemaVersion !== 1) {
207
316
  throw new KnowzError('Unsupported backup manifest schemaVersion; expected 1.', ExitCode.Usage);
208
317
  }
209
- if (value.includesIdentity !== true || !Array.isArray(value.pgDatabases) || !Array.isArray(value.volumes) || !isRecord(value.checksums)) {
318
+ if (value.includesIdentity !== true ||
319
+ !Array.isArray(value.pgDatabases) ||
320
+ !Array.isArray(value.volumes) ||
321
+ !isRecord(value.checksums)) {
210
322
  throw new KnowzError('Backup manifest is incomplete or invalid.', ExitCode.Usage);
211
323
  }
212
324
  const databases = value.pgDatabases.map(String);
213
325
  const volumes = value.volumes.map(String);
214
- if (!['KnowzMaster', 'KnowzKnowledge'].every(database => databases.includes(database)) ||
215
- !['miniodata', 'objectsdata', 'identity'].every(volume => volumes.includes(volume))) {
326
+ if (!['KnowzMaster', 'KnowzKnowledge'].every((database) => databases.includes(database)) ||
327
+ !['miniodata', 'objectsdata', 'identity'].every((volume) => volumes.includes(volume))) {
216
328
  throw new KnowzError('Backup manifest does not contain every required portable data store.', ExitCode.Usage);
217
329
  }
218
330
  const checksums = Object.fromEntries(Object.entries(value.checksums).filter((entry) => typeof entry[1] === 'string'));
@@ -233,28 +345,18 @@ export function parseAndValidateManifest(json) {
233
345
  export function validateBackupChecksums(stagingDir, manifest) {
234
346
  for (const artifact of BACKUP_ARTIFACTS) {
235
347
  const expected = manifest.checksums[artifact];
236
- if (!expected || !existsSync(join(stagingDir, artifact)) || fileSha256(join(stagingDir, artifact)) !== expected) {
348
+ if (!expected ||
349
+ !existsSync(join(stagingDir, artifact)) ||
350
+ fileSha256(join(stagingDir, artifact)) !== expected) {
237
351
  throw new KnowzError(`Backup checksum validation failed for ${artifact}.`, ExitCode.Usage);
238
352
  }
239
353
  }
240
354
  }
241
355
  export function createPrivateStaging(parent) {
242
- const staging = mkdtempSync(join(parent, '.backup-'));
243
- chmodSync(staging, 0o700);
244
- return staging;
356
+ return createPrivateTempDirectory(parent, '.backup-');
245
357
  }
246
358
  export function finalizePrivateArchive(source, destination) {
247
- chmodSync(source, 0o600);
248
- try {
249
- renameSync(source, destination);
250
- }
251
- catch (error) {
252
- if (error.code !== 'EXDEV')
253
- throw error;
254
- copyFileSync(source, destination);
255
- rmSync(source, { force: true });
256
- }
257
- chmodSync(destination, 0o600);
359
+ publishPrivateFile(source, destination);
258
360
  }
259
361
  function encryptionHeader(path) {
260
362
  const fd = openSync(path, 'r');
@@ -272,13 +374,22 @@ function encryptionHeader(path) {
272
374
  catch {
273
375
  throw new KnowzError('Encrypted backup header is invalid.', ExitCode.Usage);
274
376
  }
275
- if (!isRecord(parsed) || parsed.v !== 1 || parsed.alg !== 'aes-256-gcm' || parsed.kdf !== 'scrypt' ||
276
- typeof parsed.salt !== 'string' || typeof parsed.nonce !== 'string') {
377
+ if (!isRecord(parsed) ||
378
+ parsed.v !== 1 ||
379
+ parsed.alg !== 'aes-256-gcm' ||
380
+ parsed.kdf !== 'scrypt' ||
381
+ typeof parsed.salt !== 'string' ||
382
+ typeof parsed.nonce !== 'string') {
277
383
  throw new KnowzError('Encrypted backup header is unsupported.', ExitCode.Usage);
278
384
  }
279
385
  const tag = Buffer.alloc(16);
280
386
  readSync(fd, tag, 0, 16, size - 16);
281
- return { header: parsed, ciphertextOffset: newline + 1, size, tag };
387
+ return {
388
+ header: parsed,
389
+ ciphertextOffset: newline + 1,
390
+ size,
391
+ tag,
392
+ };
282
393
  }
283
394
  finally {
284
395
  closeSync(fd);
@@ -289,19 +400,23 @@ export async function encryptStream(input, output, passphrase) {
289
400
  const nonce = randomBytes(12);
290
401
  const key = scryptSync(passphrase, salt, 32, BACKUP_SCRYPT);
291
402
  const header = {
292
- v: 1, alg: 'aes-256-gcm', kdf: 'scrypt', salt: salt.toString('base64'), nonce: nonce.toString('base64'),
403
+ v: 1,
404
+ alg: 'aes-256-gcm',
405
+ kdf: 'scrypt',
406
+ salt: salt.toString('base64'),
407
+ nonce: nonce.toString('base64'),
293
408
  };
294
- writeFileSync(output, `${JSON.stringify(header)}\n`, { mode: 0o600 });
295
- const cipher = createCipheriv('aes-256-gcm', key, nonce);
296
- try {
297
- await pipeline(createReadStream(input), cipher, createWriteStream(output, { flags: 'a', mode: 0o600 }));
298
- writeFileSync(output, cipher.getAuthTag(), { flag: 'a', mode: 0o600 });
299
- chmodSync(output, 0o600);
300
- }
301
- catch (cause) {
302
- rmSync(output, { force: true });
303
- throw new KnowzError('Could not encrypt the backup archive.', ExitCode.Generic, { cause });
304
- }
409
+ await withPrivateOutput(output, async (temporary) => {
410
+ writeFileSync(temporary, `${JSON.stringify(header)}\n`);
411
+ const cipher = createCipheriv('aes-256-gcm', key, nonce);
412
+ try {
413
+ await pipeline(createReadStream(input), cipher, createWriteStream(temporary, { flags: 'a' }));
414
+ writeFileSync(temporary, cipher.getAuthTag(), { flag: 'a' });
415
+ }
416
+ catch (cause) {
417
+ throw new KnowzError('Could not encrypt the backup archive.', ExitCode.Generic, { cause });
418
+ }
419
+ });
305
420
  }
306
421
  export async function decryptStream(input, output, passphrase) {
307
422
  const envelope = encryptionHeader(input);
@@ -310,14 +425,14 @@ export async function decryptStream(input, output, passphrase) {
310
425
  const key = scryptSync(passphrase, salt, 32, BACKUP_SCRYPT);
311
426
  const decipher = createDecipheriv('aes-256-gcm', key, nonce);
312
427
  decipher.setAuthTag(envelope.tag);
313
- try {
314
- await pipeline(createReadStream(input, { start: envelope.ciphertextOffset, end: envelope.size - 17 }), decipher, createWriteStream(output, { mode: 0o600 }));
315
- chmodSync(output, 0o600);
316
- }
317
- catch (cause) {
318
- rmSync(output, { force: true });
319
- throw new KnowzError('Wrong passphrase or tampered archive.', ExitCode.Auth, { cause });
320
- }
428
+ await withPrivateOutput(output, async (temporary) => {
429
+ try {
430
+ await pipeline(createReadStream(input, { start: envelope.ciphertextOffset, end: envelope.size - 17 }), decipher, createWriteStream(temporary));
431
+ }
432
+ catch (cause) {
433
+ throw new KnowzError('Wrong passphrase or tampered archive.', ExitCode.Auth, { cause });
434
+ }
435
+ });
321
436
  }
322
437
  export async function runRestorePreflight(paths, projectName, opts) {
323
438
  if (!opts.yes && !opts.isTty) {
@@ -327,7 +442,11 @@ export async function runRestorePreflight(paths, projectName, opts) {
327
442
  const invocation = existsSync(paths.composeFile) && existsSync(paths.envFile)
328
443
  ? composeInvocation(paths, projectName, engine, ['ps', '--services', '--status', 'running'])
329
444
  : engineInvocation(engine, process.cwd(), [
330
- 'ps', '--filter', `label=com.docker.compose.project=${projectName}`, '--format', '{{.Names}}',
445
+ 'ps',
446
+ '--filter',
447
+ `label=com.docker.compose.project=${projectName}`,
448
+ '--format',
449
+ '{{.Names}}',
331
450
  ]);
332
451
  const result = await Promise.resolve(opts.runner(invocation.command, invocation.args, invocation.options));
333
452
  if (result.status !== 0) {
@@ -370,7 +489,8 @@ function ensureBackupRuntime(paths) {
370
489
  const env = parseEnv(readFileSync(paths.envFile, 'utf8'));
371
490
  const pgHost = env.PG_HOST?.trim();
372
491
  const s3Endpoint = env.S3_ENDPOINT?.trim();
373
- if ((pgHost && pgHost !== 'postgres') || (s3Endpoint && !/^https?:\/\/minio(?::|\/|$)/i.test(s3Endpoint))) {
492
+ if ((pgHost && pgHost !== 'postgres') ||
493
+ (s3Endpoint && !/^https?:\/\/minio(?::|\/|$)/i.test(s3Endpoint))) {
374
494
  throw new KnowzError('Portable backup v1 does not support external database or object-storage backends.', ExitCode.Usage);
375
495
  }
376
496
  return env;
@@ -386,7 +506,10 @@ function resolveArchiveDestination(out, projectName, encrypted, now) {
386
506
  destination = resolve(process.cwd(), filename);
387
507
  else {
388
508
  const candidate = resolve(out);
389
- destination = existsSync(candidate) && statSync(candidate).isDirectory() ? join(candidate, filename) : candidate;
509
+ destination =
510
+ existsSync(candidate) && statSync(candidate).isDirectory()
511
+ ? join(candidate, filename)
512
+ : candidate;
390
513
  if (encrypted && !destination.endsWith('.enc'))
391
514
  destination += '.enc';
392
515
  }
@@ -398,52 +521,81 @@ function resolveArchiveDestination(out, projectName, encrypted, now) {
398
521
  }
399
522
  export async function runPortableBackup(input) {
400
523
  const env = ensureBackupRuntime(input.paths);
401
- const engine = requireEngine(input.engine);
524
+ const engine = await assertLocalEngineContext(requireEngine(input.engine), {
525
+ runner: input.runner,
526
+ savedEngine: readSavedLocalEngine(input.paths.stateFile),
527
+ });
402
528
  if (input.encrypt && !input.passphrase)
403
529
  throw new KnowzError('A backup passphrase is required for --encrypt.', ExitCode.Auth);
404
530
  const now = input.now?.() ?? new Date();
405
531
  const destination = resolveArchiveDestination(input.out, input.projectName, input.encrypt, now);
406
532
  const secrets = knownSecretValues(env);
407
- const staging = createPrivateStaging(input.paths.runtimeDir);
408
- const plainArchive = join(input.paths.runtimeDir, `.backup-${process.pid}-${Date.now()}.tar.gz`);
409
- let startedPostgres = false;
410
- let stopInvocation;
533
+ const unlock = acquireRuntimeLock(input.paths.runtimeDir);
411
534
  try {
412
- mkdirSync(join(staging, 'pg'), { recursive: true, mode: 0o700 });
413
- mkdirSync(join(staging, 'volumes'), { recursive: true, mode: 0o700 });
414
- mkdirSync(join(staging, 'runtime'), { recursive: true, mode: 0o700 });
415
- copyFileSync(input.paths.envFile, join(staging, 'runtime/.env'));
416
- copyFileSync(input.paths.stateFile, join(staging, 'runtime/state.json'));
417
- const ps = composeInvocation(input.paths, input.projectName, engine, ['ps', '--services', '--status', 'running', 'postgres']);
418
- const status = await runInvocation(ps, 'inspect postgres', input.runner, secrets);
419
- const postgresRunning = (status.stdout ?? '').split(/\r?\n/).some(service => service.trim() === 'postgres');
420
- const plan = planBackup(input.paths, input.projectName, env, { postgresRunning, engine, stagingDir: staging });
421
- stopInvocation = plan.find(step => step.description === 'stop postgres')?.invocation;
422
- startedPostgres = !postgresRunning;
423
- await executeBackupSteps(plan.filter(step => step.description !== 'stop postgres'), input.runner, secrets);
424
- const manifest = buildManifest(input.projectName, env, input.cliVersion, input.encrypt, staging);
425
- writeFileSync(join(staging, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
426
- await runInvocation({ command: 'tar', args: ['-czf', plainArchive, '-C', staging, '.'], options: { cwd: input.paths.runtimeDir, shell: false } }, 'create backup archive', input.runner, secrets);
427
- chmodSync(plainArchive, 0o600);
428
- if (input.encrypt) {
429
- await encryptStream(plainArchive, destination, input.passphrase);
430
- rmSync(plainArchive, { force: true });
535
+ const staging = createPrivateStaging(input.paths.runtimeDir);
536
+ const plainArchive = join(input.paths.runtimeDir, `.backup-${process.pid}-${Date.now()}.tar.gz`);
537
+ let startedPostgres = false;
538
+ let stopInvocation;
539
+ try {
540
+ ensurePrivateDirectory(join(staging, 'pg'));
541
+ ensurePrivateDirectory(join(staging, 'volumes'));
542
+ ensurePrivateDirectory(join(staging, 'runtime'));
543
+ writePrivateFile(join(staging, 'runtime/.env'), readFileSync(input.paths.envFile, 'utf8'));
544
+ writePrivateFile(join(staging, 'runtime/state.json'), readFileSync(input.paths.stateFile, 'utf8'));
545
+ const ps = composeInvocation(input.paths, input.projectName, engine, [
546
+ 'ps',
547
+ '--services',
548
+ '--status',
549
+ 'running',
550
+ 'postgres',
551
+ ]);
552
+ const status = await runInvocation(ps, 'inspect postgres', input.runner, secrets);
553
+ const postgresRunning = (status.stdout ?? '')
554
+ .split(/\r?\n/)
555
+ .some((service) => service.trim() === 'postgres');
556
+ const plan = planBackup(input.paths, input.projectName, env, {
557
+ postgresRunning,
558
+ engine,
559
+ stagingDir: staging,
560
+ });
561
+ stopInvocation = plan.find((step) => step.description === 'stop postgres')?.invocation;
562
+ startedPostgres = !postgresRunning;
563
+ await executeBackupSteps(plan.filter((step) => step.description !== 'stop postgres'), input.runner, secrets);
564
+ const manifest = buildManifest(input.projectName, env, input.cliVersion, input.encrypt, staging);
565
+ writeFileSync(join(staging, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, {
566
+ mode: 0o600,
567
+ });
568
+ await runInvocation({
569
+ command: 'tar',
570
+ args: ['-czf', plainArchive, '-C', staging, '.'],
571
+ options: { cwd: input.paths.runtimeDir, shell: false },
572
+ }, 'create backup archive', input.runner, secrets);
573
+ protectPrivatePath(plainArchive);
574
+ if (input.encrypt) {
575
+ await encryptStream(plainArchive, destination, input.passphrase);
576
+ rmSync(plainArchive, { force: true });
577
+ }
578
+ else {
579
+ finalizePrivateArchive(plainArchive, destination);
580
+ }
581
+ input.emit?.(BACKUP_SECURITY_WARNING);
582
+ return { archive: destination, encrypted: input.encrypt, warning: BACKUP_SECURITY_WARNING };
431
583
  }
432
- else {
433
- finalizePrivateArchive(plainArchive, destination);
584
+ finally {
585
+ if (startedPostgres && stopInvocation) {
586
+ try {
587
+ await runInvocation(stopInvocation, 'stop postgres', input.runner, secrets);
588
+ }
589
+ catch {
590
+ /* Preserve the primary result/error. */
591
+ }
592
+ }
593
+ rmSync(staging, { recursive: true, force: true });
594
+ rmSync(plainArchive, { force: true });
434
595
  }
435
- input.emit?.(BACKUP_SECURITY_WARNING);
436
- return { archive: destination, encrypted: input.encrypt, warning: BACKUP_SECURITY_WARNING };
437
596
  }
438
597
  finally {
439
- if (startedPostgres && stopInvocation) {
440
- try {
441
- await runInvocation(stopInvocation, 'stop postgres', input.runner, secrets);
442
- }
443
- catch { /* Preserve the primary result/error. */ }
444
- }
445
- rmSync(staging, { recursive: true, force: true });
446
- rmSync(plainArchive, { force: true });
598
+ unlock();
447
599
  }
448
600
  }
449
601
  function validateTarListing(listing) {
@@ -479,12 +631,31 @@ function installPortableAssets(paths) {
479
631
  if (![compose, contract, postgres].every(existsSync)) {
480
632
  throw new KnowzError('Packaged portable runtime assets are incomplete.', ExitCode.Generic);
481
633
  }
482
- mkdirSync(paths.runtimeDir, { recursive: true, mode: 0o700 });
634
+ ensurePrivateDirectory(paths.runtimeDir);
483
635
  copyFileSync(compose, paths.composeFile);
484
636
  copyFileSync(contract, join(paths.runtimeDir, 'runtime-contract.json'));
485
637
  rmSync(paths.postgresInitDir, { recursive: true, force: true });
486
638
  cpSync(postgres, paths.postgresInitDir, { recursive: true });
487
639
  }
640
+ export function rewriteRestoredRuntimeState(json, name, engine) {
641
+ let state;
642
+ try {
643
+ state = JSON.parse(json);
644
+ if (!state || Array.isArray(state) || typeof state !== 'object')
645
+ throw new Error();
646
+ }
647
+ catch {
648
+ throw new KnowzError('Archive runtime state is invalid JSON.', ExitCode.Usage);
649
+ }
650
+ return (JSON.stringify({
651
+ ...state,
652
+ name,
653
+ engine,
654
+ healthPassed: false,
655
+ restoredAt: new Date().toISOString(),
656
+ updatedAt: new Date().toISOString(),
657
+ }, null, 2) + '\n');
658
+ }
488
659
  export async function runPortableRestore(input) {
489
660
  if (!input.yes && !input.isTty) {
490
661
  throw new KnowzError('Refusing to restore without --yes (no TTY).', ExitCode.Usage);
@@ -494,20 +665,34 @@ export async function runPortableRestore(input) {
494
665
  throw new KnowzError(`Backup archive not found: ${archive}`, ExitCode.Usage);
495
666
  if (archive.endsWith('.enc') && !input.passphrase)
496
667
  throw new KnowzError('A passphrase is required to restore this encrypted backup.', ExitCode.Auth);
497
- const engine = requireEngine(input.engine);
498
- const stagingParent = mkdtempSync(join(tmpdir(), 'knowz-restore-'));
499
- chmodSync(stagingParent, 0o700);
668
+ const engine = await assertLocalEngineContext(requireEngine(input.engine), {
669
+ runner: input.runner,
670
+ savedEngine: readSavedLocalEngine(input.paths.stateFile),
671
+ });
672
+ const stagingParent = createPrivateTempDirectory(tmpdir(), 'knowz-restore-');
500
673
  const extracted = join(stagingParent, 'extracted');
501
- mkdirSync(extracted, { mode: 0o700 });
674
+ ensurePrivateDirectory(extracted);
502
675
  const plainArchive = archive.endsWith('.enc') ? join(stagingParent, 'backup.tar.gz') : archive;
503
676
  try {
504
677
  if (archive.endsWith('.enc'))
505
678
  await decryptStream(archive, plainArchive, input.passphrase);
506
- const list = await runInvocation({ command: 'tar', args: ['-tzf', plainArchive], options: { cwd: stagingParent, shell: false } }, 'inspect backup archive', input.runner, []);
679
+ const list = await runInvocation({
680
+ command: 'tar',
681
+ args: ['-tzf', plainArchive],
682
+ options: { cwd: stagingParent, shell: false },
683
+ }, 'inspect backup archive', input.runner, []);
507
684
  validateTarListing(list.stdout ?? '');
508
- const verboseList = await runInvocation({ command: 'tar', args: ['-tvzf', plainArchive], options: { cwd: stagingParent, shell: false } }, 'inspect backup archive entry types', input.runner, []);
685
+ const verboseList = await runInvocation({
686
+ command: 'tar',
687
+ args: ['-tvzf', plainArchive],
688
+ options: { cwd: stagingParent, shell: false },
689
+ }, 'inspect backup archive entry types', input.runner, []);
509
690
  validateTarEntryTypes(verboseList.stdout ?? '');
510
- await runInvocation({ command: 'tar', args: ['-xzf', plainArchive, '--no-same-owner', '--no-same-permissions', '-C', extracted], options: { cwd: stagingParent, shell: false } }, 'extract backup archive', input.runner, []);
691
+ await runInvocation({
692
+ command: 'tar',
693
+ args: ['-xzf', plainArchive, '--no-same-owner', '--no-same-permissions', '-C', extracted],
694
+ options: { cwd: stagingParent, shell: false },
695
+ }, 'extract backup archive', input.runner, []);
511
696
  const manifestPath = join(extracted, 'manifest.json');
512
697
  if (!existsSync(manifestPath))
513
698
  throw new KnowzError('Backup archive has no manifest.json.', ExitCode.Usage);
@@ -522,7 +707,10 @@ export async function runPortableRestore(input) {
522
707
  input.emit?.(identityWarning);
523
708
  input.emit?.('Data Protection keys are deliberately excluded from portable backups. Re-enter federation, Twilio, phone-pool, and tenant-resource credentials after restore.');
524
709
  await runRestorePreflight(input.paths, input.projectName, {
525
- yes: input.yes, isTty: input.isTty, engine, runner: input.runner,
710
+ yes: input.yes,
711
+ isTty: input.isTty,
712
+ engine,
713
+ runner: input.runner,
526
714
  });
527
715
  if (!input.yes) {
528
716
  const confirmed = await input.confirmName?.();
@@ -530,47 +718,77 @@ export async function runPortableRestore(input) {
530
718
  throw new KnowzError('Restore confirmation did not match the platform name.', ExitCode.Usage);
531
719
  }
532
720
  const runtimeExisted = existsSync(input.paths.runtimeDir);
533
- if (!runtimeExisted)
534
- installPortableAssets(input.paths);
535
- else if (!existsSync(input.paths.composeFile))
536
- installPortableAssets(input.paths);
537
- if (!existsSync(input.paths.envFile))
538
- writePrivateFile(input.paths.envFile, readFileSync(join(extracted, 'runtime/.env'), 'utf8'));
539
- else
540
- input.emit?.('Existing runtime .env preserved; the archive copy was skipped.');
541
- if (!existsSync(input.paths.stateFile))
542
- writePrivateFile(input.paths.stateFile, readFileSync(join(extracted, 'runtime/state.json'), 'utf8'));
543
- const env = parseEnv(readFileSync(input.paths.envFile, 'utf8'));
544
- const secrets = knownSecretValues(env);
545
- const plan = planRestore(input.paths, input.projectName, manifest, {
546
- mode: input.mode, runtimeDirExists: runtimeExisted, engine, stagingDir: extracted, pgUser: env.PG_USER || 'knowz',
547
- });
548
- const stop = plan.find(step => step.description === 'stop postgres')?.invocation;
549
- let postgresStarted = false;
721
+ const unlock = acquireRuntimeLock(input.paths.runtimeDir);
550
722
  try {
551
- for (const step of plan) {
552
- if (!step.invocation || step.description === 'stop postgres')
553
- continue;
554
- await runInvocation(step.invocation, step.description, input.runner, secrets);
555
- if (step.description === 'start postgres')
556
- postgresStarted = true;
723
+ await runRestorePreflight(input.paths, input.projectName, {
724
+ yes: input.yes,
725
+ isTty: input.isTty,
726
+ engine,
727
+ runner: input.runner,
728
+ });
729
+ if (!runtimeExisted && (existsSync(input.paths.stateFile) || existsSync(input.paths.envFile)))
730
+ throw new KnowzError('Restore target changed during validation.', ExitCode.Usage);
731
+ if (!runtimeExisted)
732
+ installPortableAssets(input.paths);
733
+ else if (!existsSync(input.paths.composeFile))
734
+ installPortableAssets(input.paths);
735
+ if (!existsSync(input.paths.envFile))
736
+ writePrivateFile(input.paths.envFile, readFileSync(join(extracted, 'runtime/.env'), 'utf8'));
737
+ else
738
+ input.emit?.('Existing runtime .env preserved; the archive copy was skipped.');
739
+ if (!existsSync(input.paths.stateFile))
740
+ writePrivateFile(input.paths.stateFile, rewriteRestoredRuntimeState(readFileSync(join(extracted, 'runtime/state.json'), 'utf8'), input.projectName, engine));
741
+ const env = parseEnv(readFileSync(input.paths.envFile, 'utf8'));
742
+ const secrets = knownSecretValues(env);
743
+ const plan = planRestore(input.paths, input.projectName, manifest, {
744
+ mode: input.mode,
745
+ runtimeDirExists: runtimeExisted,
746
+ engine,
747
+ stagingDir: extracted,
748
+ pgUser: env.PG_USER || 'knowz',
749
+ });
750
+ const stop = plan.find((step) => step.description === 'stop postgres')?.invocation;
751
+ let postgresStarted = false;
752
+ try {
753
+ for (const step of plan) {
754
+ if (!step.invocation || step.description === 'stop postgres')
755
+ continue;
756
+ await runInvocation(step.invocation, step.description, input.runner, secrets);
757
+ if (step.description === 'start postgres')
758
+ postgresStarted = true;
759
+ }
557
760
  }
558
- }
559
- finally {
560
- if (postgresStarted && stop) {
561
- try {
562
- await runInvocation(stop, 'stop postgres', input.runner, secrets);
761
+ finally {
762
+ if (postgresStarted && stop) {
763
+ try {
764
+ await runInvocation(stop, 'stop postgres', input.runner, secrets);
765
+ }
766
+ catch {
767
+ /* Preserve primary failure. */
768
+ }
563
769
  }
564
- catch { /* Preserve primary failure. */ }
565
770
  }
771
+ if (input.mode === 'clone') {
772
+ const warning = 'Clone restore created an empty identity volume: this is a new seat. Run `knowz activate`, then `knowz up`.';
773
+ input.emit?.(warning);
774
+ return {
775
+ platformName: input.projectName,
776
+ mode: input.mode,
777
+ next: 'knowz activate && knowz up',
778
+ warning,
779
+ };
780
+ }
781
+ input.emit?.('Restore complete. Start the stack with `knowz up`.');
782
+ return {
783
+ platformName: input.projectName,
784
+ mode: input.mode,
785
+ next: 'knowz up',
786
+ warning: identityWarning,
787
+ };
566
788
  }
567
- if (input.mode === 'clone') {
568
- const warning = 'Clone restore created an empty identity volume: this is a new seat. Run `knowz activate`, then `knowz up`.';
569
- input.emit?.(warning);
570
- return { platformName: input.projectName, mode: input.mode, next: 'knowz activate && knowz up', warning };
789
+ finally {
790
+ unlock();
571
791
  }
572
- input.emit?.('Restore complete. Start the stack with `knowz up`.');
573
- return { platformName: input.projectName, mode: input.mode, next: 'knowz up', warning: identityWarning };
574
792
  }
575
793
  finally {
576
794
  rmSync(stagingParent, { recursive: true, force: true });