kandev 0.52.0 → 0.53.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.
package/dist/backup.js ADDED
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isProductionDb = isProductionDb;
7
+ exports.backupProductionDb = backupProductionDb;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_os_1 = __importDefault(require("node:os"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ // Prefix for dev-prod-db automatic snapshots. Distinct from the backend's
12
+ // "kandev-*" auto-snapshots and "manual-*" user snapshots so the families
13
+ // don't interfere with each other's retention policies.
14
+ const BACKUP_PREFIX = "dev-prod-db-";
15
+ const BACKUP_SUFFIX = ".db";
16
+ const MAX_BACKUPS = 5;
17
+ /**
18
+ * Returns true if the given dbPath points to a non-dev database that should
19
+ * be backed up before running dev mode. Dev-isolated databases live under
20
+ * <repo>/.kandev-dev/ and are considered disposable.
21
+ */
22
+ function isProductionDb(dbPath) {
23
+ const normalized = node_path_1.default.normalize(dbPath);
24
+ const segments = normalized.split(node_path_1.default.sep).filter(Boolean);
25
+ return !segments.includes(".kandev-dev");
26
+ }
27
+ /**
28
+ * Backs up the database at dbPath to <data-dir>/backups/ before dev mode
29
+ * runs against it. Keeps only the newest MAX_BACKUPS snapshots.
30
+ *
31
+ * Returns the path of the created backup, or null if the DB didn't exist
32
+ * or no backup was needed.
33
+ *
34
+ * The optional `homeDir` parameter is exposed for tests so they can redirect
35
+ * the backup location without mocking os.homedir(). The optional `now`
36
+ * parameter lets tests pass an explicit timestamp so back-to-back calls in
37
+ * the same millisecond produce distinct filenames + mtimes without sleeping.
38
+ */
39
+ function backupProductionDb(dbPath, homeDir, now) {
40
+ if (!node_fs_1.default.existsSync(dbPath)) {
41
+ return null;
42
+ }
43
+ const root = homeDir ?? node_os_1.default.homedir();
44
+ // Backups are always placed under ~/.kandev/data/backups/ (or the
45
+ // caller-supplied homeDir in tests), even when dbPath points elsewhere.
46
+ // This matches the dev-prod-db default flow; custom KANDEV_DATABASE_PATH
47
+ // values are advanced usage where the user is responsible for backup location.
48
+ const dataDir = node_path_1.default.join(root, ".kandev", "data");
49
+ const backupDir = node_path_1.default.join(dataDir, "backups");
50
+ node_fs_1.default.mkdirSync(backupDir, { recursive: true });
51
+ const stamp = now ?? new Date();
52
+ const ts = stamp.toISOString().replace(/[:.]/g, "");
53
+ const name = `${BACKUP_PREFIX}${ts}${BACKUP_SUFFIX}`;
54
+ const dest = node_path_1.default.join(backupDir, name);
55
+ node_fs_1.default.copyFileSync(dbPath, dest);
56
+ // Stamp both atime + mtime so pruning (which sorts by mtime) is deterministic.
57
+ node_fs_1.default.utimesSync(dest, stamp, stamp);
58
+ pruneBackups(backupDir, MAX_BACKUPS);
59
+ return dest;
60
+ }
61
+ /**
62
+ * Keeps only the `keep` newest dev-prod-db backup files in `dir`, deleting
63
+ * older ones. Non-matching files are left untouched.
64
+ */
65
+ function pruneBackups(dir, keep) {
66
+ let entries;
67
+ try {
68
+ entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
69
+ }
70
+ catch {
71
+ return;
72
+ }
73
+ const files = entries
74
+ .filter((e) => e.isFile() && e.name.startsWith(BACKUP_PREFIX) && e.name.endsWith(BACKUP_SUFFIX))
75
+ .map((e) => {
76
+ const fullPath = node_path_1.default.join(dir, e.name);
77
+ try {
78
+ const stat = node_fs_1.default.statSync(fullPath);
79
+ return { path: fullPath, mtime: stat.mtime };
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ })
85
+ .filter((f) => f !== null);
86
+ if (files.length <= keep) {
87
+ return;
88
+ }
89
+ files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
90
+ for (const f of files.slice(keep)) {
91
+ try {
92
+ node_fs_1.default.unlinkSync(f.path);
93
+ }
94
+ catch {
95
+ // Non-critical: don't fail the launch if one old backup can't be removed.
96
+ }
97
+ }
98
+ }
package/dist/dev.js CHANGED
@@ -8,6 +8,7 @@ exports.resolveDevBackendEnv = resolveDevBackendEnv;
8
8
  const node_child_process_1 = require("node:child_process");
9
9
  const node_fs_1 = __importDefault(require("node:fs"));
10
10
  const node_path_1 = __importDefault(require("node:path"));
11
+ const backup_1 = require("./backup");
11
12
  const constants_1 = require("./constants");
12
13
  const health_1 = require("./health");
13
14
  const kandev_env_1 = require("./kandev-env");
@@ -17,6 +18,22 @@ const web_1 = require("./web");
17
18
  async function runDev({ repoRoot, backendPort, webPort }) {
18
19
  const ports = await (0, shared_1.pickPorts)(backendPort, webPort);
19
20
  const { dbPath, extra } = resolveDevBackendEnv(repoRoot);
21
+ if ((0, backup_1.isProductionDb)(dbPath)) {
22
+ try {
23
+ const backupPath = (0, backup_1.backupProductionDb)(dbPath);
24
+ if (backupPath) {
25
+ const name = node_path_1.default.basename(backupPath);
26
+ console.log(`[kandev] backed up production db → ${name}`);
27
+ }
28
+ }
29
+ catch (err) {
30
+ const message = err instanceof Error ? err.message : String(err);
31
+ // Abort rather than continue: the backup exists precisely to protect the
32
+ // production db before dev mode touches it. Proceeding on failure would
33
+ // remove the safety guarantee that justified introducing this guard.
34
+ throw new Error(`failed to back up production db (${message}); aborting dev startup`);
35
+ }
36
+ }
20
37
  const backendEnv = (0, shared_1.buildBackendEnv)({ ports, extra });
21
38
  const webEnv = (0, shared_1.buildWebEnv)({ ports, debug: true });
22
39
  const logLevel = process.env.KANDEV_LOGGING_LEVEL?.trim() || process.env.KANDEV_LOG_LEVEL?.trim() || "info";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kandev",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "private": false,
5
5
  "description": "Launcher for Kandev — manage tasks, orchestrate agents, review changes, and ship value",
6
6
  "license": "AGPL-3.0-only",
@@ -22,11 +22,11 @@
22
22
  "npm": ">=7"
23
23
  },
24
24
  "optionalDependencies": {
25
- "@kdlbs/runtime-linux-x64": "0.52.0",
26
- "@kdlbs/runtime-linux-arm64": "0.52.0",
27
- "@kdlbs/runtime-darwin-x64": "0.52.0",
28
- "@kdlbs/runtime-darwin-arm64": "0.52.0",
29
- "@kdlbs/runtime-win32-x64": "0.52.0"
25
+ "@kdlbs/runtime-linux-x64": "0.53.0",
26
+ "@kdlbs/runtime-linux-arm64": "0.53.0",
27
+ "@kdlbs/runtime-darwin-x64": "0.53.0",
28
+ "@kdlbs/runtime-darwin-arm64": "0.53.0",
29
+ "@kdlbs/runtime-win32-x64": "0.53.0"
30
30
  },
31
31
  "dependencies": {
32
32
  "tar": "^7.5.11",