@batadata/cli 0.1.7 → 0.1.9
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/README.md +81 -0
- package/dist/api.d.ts +1 -0
- package/dist/api.js +1 -0
- package/dist/commands/connect.js +4 -2
- package/dist/commands/db.d.ts +35 -1
- package/dist/commands/db.js +242 -17
- package/dist/commands/link.d.ts +2 -0
- package/dist/commands/link.js +138 -0
- package/dist/commands/projects.js +6 -4
- package/dist/commands/restore.d.ts +71 -0
- package/dist/commands/restore.js +356 -0
- package/dist/commands/schema.js +5 -4
- package/dist/index.js +19 -0
- package/dist/link.d.ts +107 -0
- package/dist/link.js +170 -0
- package/package.json +1 -1
package/dist/link.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project linking — Vercel/Neon-style. A linked directory carries its project
|
|
3
|
+
* (and optionally its branch) in `.batadata/project.json`, so an agent no longer
|
|
4
|
+
* has to thread `--project <id>` through every invocation.
|
|
5
|
+
*
|
|
6
|
+
* ── Resolution precedence (the ONE rule, used everywhere) ─────────────────
|
|
7
|
+
* 1. an explicit `--project` / `--branch` flag (per-invocation intent)
|
|
8
|
+
* 2. `.batadata/project.json`, discovered by walking UP from CWD like git (per-directory intent)
|
|
9
|
+
* 3. `defaultProject` in `~/.batarc` (per-machine fallback)
|
|
10
|
+
*
|
|
11
|
+
* Flags always win, so linking never silently overrides an explicit request.
|
|
12
|
+
* The precedence itself lives in the pure `pickProjectId` / `pickBranchId`
|
|
13
|
+
* helpers (unit-testable without fs), and `resolveProjectId` / `resolveBranchId`
|
|
14
|
+
* compose them with the on-disk sources.
|
|
15
|
+
*/
|
|
16
|
+
export declare const LINK_DIR = ".batadata";
|
|
17
|
+
export declare const LINK_FILE = "project.json";
|
|
18
|
+
export interface LinkFile {
|
|
19
|
+
projectId: string;
|
|
20
|
+
/** Optional pinned branch (set by `bata db branch checkout`). */
|
|
21
|
+
branchId?: string | null;
|
|
22
|
+
}
|
|
23
|
+
export type ProjectSource = "flag" | "link" | "config" | "none";
|
|
24
|
+
export type BranchSource = "flag" | "link" | "none";
|
|
25
|
+
/**
|
|
26
|
+
* The ordered list of directories to search for a link file, from `startDir`
|
|
27
|
+
* up to (and including) the ceiling — the user's HOME dir or the filesystem
|
|
28
|
+
* root, whichever comes first. Exactly how git bounds its repo discovery.
|
|
29
|
+
*
|
|
30
|
+
* Pure: no fs, no env — just path math — so the walk-up order is unit-testable
|
|
31
|
+
* without ever writing to a real HOME.
|
|
32
|
+
*/
|
|
33
|
+
export declare function linkSearchDirs(startDir: string, homeDir: string): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Absolute path to the nearest `.batadata/project.json` at or above `startDir`,
|
|
36
|
+
* or null if none exists within the ceiling. Walks up like git.
|
|
37
|
+
*/
|
|
38
|
+
export declare function findLinkFile(startDir?: string, homeDir?: string): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Read and parse the nearest link file. Returns its absolute path alongside the
|
|
41
|
+
* parsed contents, or null if there's no (valid) link file. A malformed file is
|
|
42
|
+
* treated as absent rather than throwing, so a stray/corrupt file can't wedge
|
|
43
|
+
* every command.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readLinkFile(startDir?: string, homeDir?: string): {
|
|
46
|
+
path: string;
|
|
47
|
+
link: LinkFile;
|
|
48
|
+
} | null;
|
|
49
|
+
/**
|
|
50
|
+
* Write `.batadata/project.json` inside `dir` (creating `.batadata/` if needed)
|
|
51
|
+
* and return the absolute path written. `branchId` is normalized to null when
|
|
52
|
+
* absent so the file shape is stable.
|
|
53
|
+
*/
|
|
54
|
+
export declare function writeLinkFile(dir: string, link: LinkFile): string;
|
|
55
|
+
/**
|
|
56
|
+
* Delete the nearest link file (and its now-empty `.batadata/` dir, if empty).
|
|
57
|
+
* Returns the path removed, or null if there was nothing to remove. Idempotent.
|
|
58
|
+
*/
|
|
59
|
+
export declare function removeLinkFile(startDir?: string, homeDir?: string): string | null;
|
|
60
|
+
/**
|
|
61
|
+
* Pure precedence policy for the project id: flag > linked > config > none.
|
|
62
|
+
* No fs/env access, so the ordering is unit-testable in isolation (mirrors the
|
|
63
|
+
* `pickTeamId` pattern in api.ts).
|
|
64
|
+
*/
|
|
65
|
+
export declare function pickProjectId(params: {
|
|
66
|
+
flag?: string;
|
|
67
|
+
linked?: string | null;
|
|
68
|
+
config?: string;
|
|
69
|
+
}): {
|
|
70
|
+
projectId?: string;
|
|
71
|
+
source: ProjectSource;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Pure precedence policy for the branch id: flag > linked > none. There is no
|
|
75
|
+
* config-level default branch, so config isn't a source here.
|
|
76
|
+
*/
|
|
77
|
+
export declare function pickBranchId(params: {
|
|
78
|
+
flag?: string;
|
|
79
|
+
linked?: string | null;
|
|
80
|
+
}): {
|
|
81
|
+
branchId?: string;
|
|
82
|
+
source: BranchSource;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the active project id from all sources in precedence order. `explicit`
|
|
86
|
+
* is an already-parsed `--project` value (or a positional id) — pass it through
|
|
87
|
+
* so a flag always wins over a link file or config default.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveProjectId(explicit?: string, opts?: {
|
|
90
|
+
startDir?: string;
|
|
91
|
+
homeDir?: string;
|
|
92
|
+
}): {
|
|
93
|
+
projectId?: string;
|
|
94
|
+
source: ProjectSource;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the active branch id from all sources in precedence order. `explicit`
|
|
98
|
+
* is an already-parsed `--branch` value (id or name); the link file's pinned
|
|
99
|
+
* branch is the fallback.
|
|
100
|
+
*/
|
|
101
|
+
export declare function resolveBranchId(explicit?: string, opts?: {
|
|
102
|
+
startDir?: string;
|
|
103
|
+
homeDir?: string;
|
|
104
|
+
}): {
|
|
105
|
+
branchId?: string;
|
|
106
|
+
source: BranchSource;
|
|
107
|
+
};
|
package/dist/link.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Project linking — Vercel/Neon-style. A linked directory carries its project
|
|
7
|
+
* (and optionally its branch) in `.batadata/project.json`, so an agent no longer
|
|
8
|
+
* has to thread `--project <id>` through every invocation.
|
|
9
|
+
*
|
|
10
|
+
* ── Resolution precedence (the ONE rule, used everywhere) ─────────────────
|
|
11
|
+
* 1. an explicit `--project` / `--branch` flag (per-invocation intent)
|
|
12
|
+
* 2. `.batadata/project.json`, discovered by walking UP from CWD like git (per-directory intent)
|
|
13
|
+
* 3. `defaultProject` in `~/.batarc` (per-machine fallback)
|
|
14
|
+
*
|
|
15
|
+
* Flags always win, so linking never silently overrides an explicit request.
|
|
16
|
+
* The precedence itself lives in the pure `pickProjectId` / `pickBranchId`
|
|
17
|
+
* helpers (unit-testable without fs), and `resolveProjectId` / `resolveBranchId`
|
|
18
|
+
* compose them with the on-disk sources.
|
|
19
|
+
*/
|
|
20
|
+
export const LINK_DIR = ".batadata";
|
|
21
|
+
export const LINK_FILE = "project.json";
|
|
22
|
+
/** Resolve HOME at call time (env-first) so a redirected HOME — every test does
|
|
23
|
+
* this to avoid touching the developer's real home — is honored. Mirrors the
|
|
24
|
+
* same rule in config.ts. */
|
|
25
|
+
function defaultHome() {
|
|
26
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The ordered list of directories to search for a link file, from `startDir`
|
|
30
|
+
* up to (and including) the ceiling — the user's HOME dir or the filesystem
|
|
31
|
+
* root, whichever comes first. Exactly how git bounds its repo discovery.
|
|
32
|
+
*
|
|
33
|
+
* Pure: no fs, no env — just path math — so the walk-up order is unit-testable
|
|
34
|
+
* without ever writing to a real HOME.
|
|
35
|
+
*/
|
|
36
|
+
export function linkSearchDirs(startDir, homeDir) {
|
|
37
|
+
const dirs = [];
|
|
38
|
+
let dir = path.resolve(startDir);
|
|
39
|
+
const home = path.resolve(homeDir);
|
|
40
|
+
// Guard against a pathological loop; the parent === dir root check is the
|
|
41
|
+
// real terminator.
|
|
42
|
+
for (let i = 0; i < 4096; i++) {
|
|
43
|
+
dirs.push(dir);
|
|
44
|
+
if (dir === home)
|
|
45
|
+
break; // don't search above HOME
|
|
46
|
+
const parent = path.dirname(dir);
|
|
47
|
+
if (parent === dir)
|
|
48
|
+
break; // filesystem root
|
|
49
|
+
dir = parent;
|
|
50
|
+
}
|
|
51
|
+
return dirs;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Absolute path to the nearest `.batadata/project.json` at or above `startDir`,
|
|
55
|
+
* or null if none exists within the ceiling. Walks up like git.
|
|
56
|
+
*/
|
|
57
|
+
export function findLinkFile(startDir = process.cwd(), homeDir = defaultHome()) {
|
|
58
|
+
for (const dir of linkSearchDirs(startDir, homeDir)) {
|
|
59
|
+
const candidate = path.join(dir, LINK_DIR, LINK_FILE);
|
|
60
|
+
if (fs.existsSync(candidate))
|
|
61
|
+
return candidate;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Read and parse the nearest link file. Returns its absolute path alongside the
|
|
67
|
+
* parsed contents, or null if there's no (valid) link file. A malformed file is
|
|
68
|
+
* treated as absent rather than throwing, so a stray/corrupt file can't wedge
|
|
69
|
+
* every command.
|
|
70
|
+
*/
|
|
71
|
+
export function readLinkFile(startDir = process.cwd(), homeDir = defaultHome()) {
|
|
72
|
+
const filePath = findLinkFile(startDir, homeDir);
|
|
73
|
+
if (!filePath)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
77
|
+
const parsed = JSON.parse(raw);
|
|
78
|
+
if (!parsed || typeof parsed.projectId !== "string" || !parsed.projectId) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return { path: filePath, link: parsed };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Write `.batadata/project.json` inside `dir` (creating `.batadata/` if needed)
|
|
89
|
+
* and return the absolute path written. `branchId` is normalized to null when
|
|
90
|
+
* absent so the file shape is stable.
|
|
91
|
+
*/
|
|
92
|
+
export function writeLinkFile(dir, link) {
|
|
93
|
+
const linkDir = path.join(path.resolve(dir), LINK_DIR);
|
|
94
|
+
fs.mkdirSync(linkDir, { recursive: true });
|
|
95
|
+
const filePath = path.join(linkDir, LINK_FILE);
|
|
96
|
+
const body = {
|
|
97
|
+
projectId: link.projectId,
|
|
98
|
+
branchId: link.branchId ?? null,
|
|
99
|
+
};
|
|
100
|
+
fs.writeFileSync(filePath, JSON.stringify(body, null, 2) + "\n", "utf-8");
|
|
101
|
+
return filePath;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Delete the nearest link file (and its now-empty `.batadata/` dir, if empty).
|
|
105
|
+
* Returns the path removed, or null if there was nothing to remove. Idempotent.
|
|
106
|
+
*/
|
|
107
|
+
export function removeLinkFile(startDir = process.cwd(), homeDir = defaultHome()) {
|
|
108
|
+
const filePath = findLinkFile(startDir, homeDir);
|
|
109
|
+
if (!filePath)
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
fs.unlinkSync(filePath);
|
|
113
|
+
// Best-effort cleanup of an empty .batadata dir.
|
|
114
|
+
const linkDir = path.dirname(filePath);
|
|
115
|
+
if (path.basename(linkDir) === LINK_DIR && fs.readdirSync(linkDir).length === 0) {
|
|
116
|
+
fs.rmdirSync(linkDir);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Already gone / racing removal — unlink is best-effort.
|
|
121
|
+
}
|
|
122
|
+
return filePath;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Pure precedence policy for the project id: flag > linked > config > none.
|
|
126
|
+
* No fs/env access, so the ordering is unit-testable in isolation (mirrors the
|
|
127
|
+
* `pickTeamId` pattern in api.ts).
|
|
128
|
+
*/
|
|
129
|
+
export function pickProjectId(params) {
|
|
130
|
+
if (params.flag)
|
|
131
|
+
return { projectId: params.flag, source: "flag" };
|
|
132
|
+
if (params.linked)
|
|
133
|
+
return { projectId: params.linked, source: "link" };
|
|
134
|
+
if (params.config)
|
|
135
|
+
return { projectId: params.config, source: "config" };
|
|
136
|
+
return { source: "none" };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Pure precedence policy for the branch id: flag > linked > none. There is no
|
|
140
|
+
* config-level default branch, so config isn't a source here.
|
|
141
|
+
*/
|
|
142
|
+
export function pickBranchId(params) {
|
|
143
|
+
if (params.flag)
|
|
144
|
+
return { branchId: params.flag, source: "flag" };
|
|
145
|
+
if (params.linked)
|
|
146
|
+
return { branchId: params.linked, source: "link" };
|
|
147
|
+
return { source: "none" };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Resolve the active project id from all sources in precedence order. `explicit`
|
|
151
|
+
* is an already-parsed `--project` value (or a positional id) — pass it through
|
|
152
|
+
* so a flag always wins over a link file or config default.
|
|
153
|
+
*/
|
|
154
|
+
export function resolveProjectId(explicit, opts = {}) {
|
|
155
|
+
const linked = readLinkFile(opts.startDir, opts.homeDir)?.link.projectId ?? null;
|
|
156
|
+
return pickProjectId({
|
|
157
|
+
flag: explicit,
|
|
158
|
+
linked,
|
|
159
|
+
config: loadConfig().defaultProject,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the active branch id from all sources in precedence order. `explicit`
|
|
164
|
+
* is an already-parsed `--branch` value (id or name); the link file's pinned
|
|
165
|
+
* branch is the fallback.
|
|
166
|
+
*/
|
|
167
|
+
export function resolveBranchId(explicit, opts = {}) {
|
|
168
|
+
const linked = readLinkFile(opts.startDir, opts.homeDir)?.link.branchId ?? null;
|
|
169
|
+
return pickBranchId({ flag: explicit, linked });
|
|
170
|
+
}
|