local-kv-cli 1.0.0 → 2.0.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.
@@ -0,0 +1,19 @@
1
+ export declare const PROJECT_FILE = ".lkv.json";
2
+ export type StoreKind = "env" | "project" | "global";
3
+ export interface StoreLocation {
4
+ path: string;
5
+ kind: StoreKind;
6
+ }
7
+ export declare function globalStorePath(): string;
8
+ /** Walk up from `start` looking for a project store file. */
9
+ export declare function findProjectStore(start: string): string | undefined;
10
+ /**
11
+ * Which store to use, in order of priority:
12
+ * 1. LKV_STORE_PATH env variable
13
+ * 2. a .lkv.json in this folder or a parent (skipped with `global`)
14
+ * 3. the global store in the home folder
15
+ */
16
+ export declare function locateStore(options?: {
17
+ cwd?: string;
18
+ global?: boolean;
19
+ }): StoreLocation;
package/dist/locate.js ADDED
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PROJECT_FILE = void 0;
37
+ exports.globalStorePath = globalStorePath;
38
+ exports.findProjectStore = findProjectStore;
39
+ exports.locateStore = locateStore;
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ exports.PROJECT_FILE = ".lkv.json";
44
+ function globalStorePath() {
45
+ return path.join(os.homedir(), ".local-kv", "store.json");
46
+ }
47
+ /** Walk up from `start` looking for a project store file. */
48
+ function findProjectStore(start) {
49
+ let dir = path.resolve(start);
50
+ for (;;) {
51
+ const candidate = path.join(dir, exports.PROJECT_FILE);
52
+ if (fs.existsSync(candidate))
53
+ return candidate;
54
+ const parent = path.dirname(dir);
55
+ if (parent === dir)
56
+ return undefined;
57
+ dir = parent;
58
+ }
59
+ }
60
+ /**
61
+ * Which store to use, in order of priority:
62
+ * 1. LKV_STORE_PATH env variable
63
+ * 2. a .lkv.json in this folder or a parent (skipped with `global`)
64
+ * 3. the global store in the home folder
65
+ */
66
+ function locateStore(options = {}) {
67
+ if (process.env.LKV_STORE_PATH)
68
+ return { path: process.env.LKV_STORE_PATH, kind: "env" };
69
+ if (!options.global) {
70
+ const project = findProjectStore(options.cwd ?? process.cwd());
71
+ if (project)
72
+ return { path: project, kind: "project" };
73
+ }
74
+ return { path: globalStorePath(), kind: "global" };
75
+ }
package/dist/store.d.ts CHANGED
@@ -1,20 +1,72 @@
1
- export type Data = Record<string, string>;
2
- /** Where the data lives. Override with the LKV_STORE_PATH env variable. */
3
- export declare function getStorePath(): string;
1
+ import { Entry, HistoryItem, StoreFile } from "./format";
2
+ import { StoreKind } from "./locate";
3
+ import { Value } from "./values";
4
+ export interface StoreOptions {
5
+ /** Use this exact file instead of looking one up. */
6
+ path?: string;
7
+ /** Namespace to work in. Defaults to LKV_NAMESPACE or "default". */
8
+ namespace?: string;
9
+ /** Folder to start looking for a project .lkv.json from. */
10
+ cwd?: string;
11
+ /** Ignore project stores and use the global one. */
12
+ global?: boolean;
13
+ }
14
+ export interface ImportResult {
15
+ added: number;
16
+ overwritten: number;
17
+ skipped: number;
18
+ }
4
19
  export declare class Store {
5
- private readonly filePath;
6
- constructor(filePath?: string);
7
- get path(): string;
8
- private read;
9
- private write;
20
+ readonly path: string;
21
+ readonly kind: StoreKind | "custom";
22
+ readonly namespace: string;
23
+ /** Set to the backup file's path if a v1 store was upgraded while loading. */
24
+ migratedBackup?: string;
25
+ constructor(options?: StoreOptions | string);
10
26
  has(key: string): boolean;
11
- /** Create a new pair. Throws if the key already exists (use update, or force). */
12
- set(key: string, value: string, force?: boolean): void;
13
- get(key: string): string | undefined;
27
+ get(key: string): Value | undefined;
28
+ /** The value plus its type, timestamps and history. */
29
+ entry(key: string): Entry | undefined;
30
+ /** Previous values of a key, oldest first (not including the current value). */
31
+ history(key: string): HistoryItem[] | undefined;
32
+ all(): Record<string, Value>;
33
+ entries(): Record<string, Entry>;
34
+ /** Case-insensitive match on keys and values. */
35
+ search(text: string): Record<string, Value>;
36
+ /** Namespace names with the number of keys in each. */
37
+ namespaces(): Record<string, number>;
38
+ /** Create a pair. Throws if the key already exists, unless `force` is set. */
39
+ set(key: string, value: Value, options?: {
40
+ force?: boolean;
41
+ }): void;
14
42
  /** Change an existing pair. Throws if the key does not exist. */
15
- update(key: string, value: string): void;
43
+ update(key: string, value: Value): void;
16
44
  /** Returns true if the key existed and was removed. */
17
45
  delete(key: string): boolean;
18
- all(): Data;
19
- clear(): void;
46
+ rename(from: string, to: string, options?: {
47
+ force?: boolean;
48
+ }): void;
49
+ /** Add `by` to a number (a missing key starts at 0). Returns the new value. */
50
+ incr(key: string, by?: number): number;
51
+ /** Remove every pair in the current namespace. Returns how many were removed. */
52
+ clear(): number;
53
+ /** The current namespace (or all of them) in the store file format. */
54
+ export(all?: boolean): StoreFile;
55
+ /**
56
+ * Import data from an export (keeps namespaces, types and history)
57
+ * or a plain { key: value } object (goes into the current namespace).
58
+ * Existing keys are skipped unless `force` is set.
59
+ */
60
+ import(data: unknown, options?: {
61
+ force?: boolean;
62
+ }): ImportResult;
63
+ private readNamespace;
64
+ private load;
65
+ private save;
66
+ private mutate;
20
67
  }
68
+ /**
69
+ * Read text that may be UTF-8 or UTF-16 (Windows PowerShell 5's `>` writes UTF-16),
70
+ * with or without a byte order mark.
71
+ */
72
+ export declare function decodeText(buf: Buffer): string;
package/dist/store.js CHANGED
@@ -34,83 +34,287 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.Store = void 0;
37
- exports.getStorePath = getStorePath;
37
+ exports.decodeText = decodeText;
38
38
  const fs = __importStar(require("fs"));
39
- const os = __importStar(require("os"));
40
39
  const path = __importStar(require("path"));
41
- /** Where the data lives. Override with the LKV_STORE_PATH env variable. */
42
- function getStorePath() {
43
- return process.env.LKV_STORE_PATH || path.join(os.homedir(), ".local-kv", "store.json");
44
- }
40
+ const format_1 = require("./format");
41
+ const locate_1 = require("./locate");
42
+ const values_1 = require("./values");
45
43
  class Store {
46
- filePath;
47
- constructor(filePath = getStorePath()) {
48
- this.filePath = filePath;
49
- }
50
- get path() {
51
- return this.filePath;
52
- }
53
- read() {
54
- // Prototype-less object so keys like "constructor" or "__proto__" are just normal keys.
55
- const data = Object.create(null);
56
- if (!fs.existsSync(this.filePath))
57
- return data;
58
- const raw = fs.readFileSync(this.filePath, "utf8");
59
- if (!raw.trim())
60
- return data;
61
- try {
62
- return Object.assign(data, JSON.parse(raw));
44
+ path;
45
+ kind;
46
+ namespace;
47
+ /** Set to the backup file's path if a v1 store was upgraded while loading. */
48
+ migratedBackup;
49
+ constructor(options = {}) {
50
+ const opts = typeof options === "string" ? { path: options } : options;
51
+ if (opts.path) {
52
+ this.path = opts.path;
53
+ this.kind = "custom";
63
54
  }
64
- catch {
65
- throw new Error(`Store file is corrupted: ${this.filePath}`);
55
+ else {
56
+ const location = (0, locate_1.locateStore)(opts);
57
+ this.path = location.path;
58
+ this.kind = location.kind;
66
59
  }
60
+ this.namespace = opts.namespace || process.env.LKV_NAMESPACE || format_1.DEFAULT_NAMESPACE;
67
61
  }
68
- write(data) {
69
- fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
70
- // Write to a temp file then rename, so a crash never leaves a half-written store.
71
- const tmp = `${this.filePath}.tmp`;
72
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
73
- fs.renameSync(tmp, this.filePath);
74
- }
62
+ // ---- reading ----
75
63
  has(key) {
76
- return key in this.read();
64
+ return key in this.readNamespace();
77
65
  }
78
- /** Create a new pair. Throws if the key already exists (use update, or force). */
79
- set(key, value, force = false) {
80
- const data = this.read();
81
- if (!force && key in data) {
82
- throw new Error(`Key "${key}" already exists. Use "update" to change it.`);
66
+ get(key) {
67
+ return this.entry(key)?.value;
68
+ }
69
+ /** The value plus its type, timestamps and history. */
70
+ entry(key) {
71
+ const ns = this.readNamespace();
72
+ return key in ns ? ns[key] : undefined;
73
+ }
74
+ /** Previous values of a key, oldest first (not including the current value). */
75
+ history(key) {
76
+ return this.entry(key)?.history;
77
+ }
78
+ all() {
79
+ const ns = this.readNamespace();
80
+ const out = (0, format_1.dict)();
81
+ for (const key of Object.keys(ns))
82
+ out[key] = ns[key].value;
83
+ return out;
84
+ }
85
+ entries() {
86
+ return this.readNamespace();
87
+ }
88
+ /** Case-insensitive match on keys and values. */
89
+ search(text) {
90
+ const needle = text.toLowerCase();
91
+ const out = (0, format_1.dict)();
92
+ const all = this.all();
93
+ for (const key of Object.keys(all)) {
94
+ const asText = typeof all[key] === "string" ? all[key] : JSON.stringify(all[key]);
95
+ if (key.toLowerCase().includes(needle) || asText.toLowerCase().includes(needle)) {
96
+ out[key] = all[key];
97
+ }
83
98
  }
84
- data[key] = value;
85
- this.write(data);
99
+ return out;
86
100
  }
87
- get(key) {
88
- const data = this.read();
89
- return key in data ? data[key] : undefined;
101
+ /** Namespace names with the number of keys in each. */
102
+ namespaces() {
103
+ const file = this.load();
104
+ const out = (0, format_1.dict)();
105
+ for (const name of Object.keys(file.namespaces)) {
106
+ out[name] = Object.keys(file.namespaces[name]).length;
107
+ }
108
+ if (!(this.namespace in out))
109
+ out[this.namespace] = 0;
110
+ return out;
111
+ }
112
+ // ---- writing ----
113
+ /** Create a pair. Throws if the key already exists, unless `force` is set. */
114
+ set(key, value, options = {}) {
115
+ checkKey(key);
116
+ this.mutate((ns, now) => {
117
+ if (key in ns) {
118
+ if (!options.force)
119
+ throw new Error(`Key "${key}" already exists. Use "update" to change it.`);
120
+ changeValue(ns[key], value, now);
121
+ }
122
+ else {
123
+ ns[key] = (0, format_1.newEntry)(value, now);
124
+ }
125
+ });
90
126
  }
91
127
  /** Change an existing pair. Throws if the key does not exist. */
92
128
  update(key, value) {
93
- const data = this.read();
94
- if (!(key in data)) {
95
- throw new Error(`Key "${key}" not found. Use "set" to create it.`);
96
- }
97
- data[key] = value;
98
- this.write(data);
129
+ this.mutate((ns, now) => {
130
+ if (!(key in ns))
131
+ throw new Error(`Key "${key}" not found. Use "set" to create it.`);
132
+ changeValue(ns[key], value, now);
133
+ });
99
134
  }
100
135
  /** Returns true if the key existed and was removed. */
101
136
  delete(key) {
102
- const data = this.read();
103
- if (!(key in data))
104
- return false;
105
- delete data[key];
106
- this.write(data);
107
- return true;
137
+ return this.mutate((ns) => {
138
+ if (!(key in ns))
139
+ return false;
140
+ delete ns[key];
141
+ return true;
142
+ });
108
143
  }
109
- all() {
110
- return this.read();
144
+ rename(from, to, options = {}) {
145
+ checkKey(to);
146
+ this.mutate((ns) => {
147
+ if (!(from in ns))
148
+ throw new Error(`Key "${from}" not found`);
149
+ if (from === to)
150
+ return;
151
+ if (to in ns && !options.force) {
152
+ throw new Error(`Key "${to}" already exists. Use --force to overwrite it.`);
153
+ }
154
+ ns[to] = ns[from];
155
+ delete ns[from];
156
+ });
157
+ }
158
+ /** Add `by` to a number (a missing key starts at 0). Returns the new value. */
159
+ incr(key, by = 1) {
160
+ checkKey(key);
161
+ if (!Number.isFinite(by))
162
+ throw new Error("Amount must be a number");
163
+ return this.mutate((ns, now) => {
164
+ if (!(key in ns)) {
165
+ ns[key] = (0, format_1.newEntry)(by, now);
166
+ return by;
167
+ }
168
+ const current = ns[key].value;
169
+ if (typeof current !== "number") {
170
+ throw new Error(`Key "${key}" is a ${ns[key].type}, not a number`);
171
+ }
172
+ const next = current + by;
173
+ changeValue(ns[key], next, now);
174
+ return next;
175
+ });
111
176
  }
177
+ /** Remove every pair in the current namespace. Returns how many were removed. */
112
178
  clear() {
113
- this.write({});
179
+ return this.mutate((ns) => {
180
+ const keys = Object.keys(ns);
181
+ for (const key of keys)
182
+ delete ns[key];
183
+ return keys.length;
184
+ });
185
+ }
186
+ // ---- backup ----
187
+ /** The current namespace (or all of them) in the store file format. */
188
+ export(all = false) {
189
+ const file = this.load();
190
+ if (all)
191
+ return file;
192
+ return {
193
+ version: format_1.FORMAT_VERSION,
194
+ namespaces: (0, format_1.dict)({ [this.namespace]: file.namespaces[this.namespace] ?? (0, format_1.dict)() }),
195
+ };
196
+ }
197
+ /**
198
+ * Import data from an export (keeps namespaces, types and history)
199
+ * or a plain { key: value } object (goes into the current namespace).
200
+ * Existing keys are skipped unless `force` is set.
201
+ */
202
+ import(data, options = {}) {
203
+ if (!(0, format_1.isPlainObject)(data))
204
+ throw new Error("Import data must be a JSON object");
205
+ const now = new Date().toISOString();
206
+ const incoming = (0, format_1.isV2)(data)
207
+ ? (0, format_1.normalizeV2)(data, now).namespaces
208
+ : (0, format_1.dict)({ [this.namespace]: plainToNamespace(data, now) });
209
+ const result = { added: 0, overwritten: 0, skipped: 0 };
210
+ this.mutate((_, __, file) => {
211
+ for (const name of Object.keys(incoming)) {
212
+ const target = (file.namespaces[name] ??= (0, format_1.dict)());
213
+ for (const key of Object.keys(incoming[name])) {
214
+ if (key in target) {
215
+ if (!options.force) {
216
+ result.skipped++;
217
+ continue;
218
+ }
219
+ result.overwritten++;
220
+ }
221
+ else {
222
+ result.added++;
223
+ }
224
+ target[key] = incoming[name][key];
225
+ }
226
+ }
227
+ });
228
+ return result;
229
+ }
230
+ // ---- file handling ----
231
+ readNamespace() {
232
+ return this.load().namespaces[this.namespace] ?? (0, format_1.dict)();
233
+ }
234
+ load() {
235
+ if (!fs.existsSync(this.path))
236
+ return (0, format_1.emptyFile)();
237
+ const raw = decodeText(fs.readFileSync(this.path));
238
+ if (!raw.trim())
239
+ return (0, format_1.emptyFile)();
240
+ let data;
241
+ try {
242
+ data = JSON.parse(raw);
243
+ }
244
+ catch {
245
+ throw new Error(`Store file is corrupted: ${this.path}`);
246
+ }
247
+ const now = new Date().toISOString();
248
+ if ((0, format_1.isV2)(data))
249
+ return (0, format_1.normalizeV2)(data, now);
250
+ if (!(0, format_1.isPlainObject)(data))
251
+ throw new Error(`Store file is corrupted: ${this.path}`);
252
+ // A v1 store: keep a backup, then upgrade it in place.
253
+ const backup = `${this.path}.v1-backup`;
254
+ fs.copyFileSync(this.path, backup);
255
+ const upgraded = (0, format_1.migrateV1)(data, now);
256
+ this.save(upgraded);
257
+ this.migratedBackup = backup;
258
+ return upgraded;
259
+ }
260
+ save(file) {
261
+ fs.mkdirSync(path.dirname(this.path), { recursive: true });
262
+ // Write to a temp file then rename, so a crash never leaves a half-written store.
263
+ const tmp = `${this.path}.tmp`;
264
+ fs.writeFileSync(tmp, JSON.stringify(file, null, 2));
265
+ fs.renameSync(tmp, this.path);
266
+ }
267
+ mutate(fn) {
268
+ const file = this.load();
269
+ const ns = (file.namespaces[this.namespace] ??= (0, format_1.dict)());
270
+ const result = fn(ns, new Date().toISOString(), file);
271
+ // Don't leave empty namespaces lying around (except the default one).
272
+ for (const name of Object.keys(file.namespaces)) {
273
+ if (name !== format_1.DEFAULT_NAMESPACE && Object.keys(file.namespaces[name]).length === 0) {
274
+ delete file.namespaces[name];
275
+ }
276
+ }
277
+ this.save(file);
278
+ return result;
114
279
  }
115
280
  }
116
281
  exports.Store = Store;
282
+ function checkKey(key) {
283
+ if (typeof key !== "string" || key === "")
284
+ throw new Error("Key must be a non-empty string");
285
+ }
286
+ function changeValue(entry, value, now) {
287
+ if (JSON.stringify(entry.value) !== JSON.stringify(value)) {
288
+ entry.history.push({ value: entry.value, type: entry.type, updated: entry.updated });
289
+ if (entry.history.length > format_1.HISTORY_LIMIT)
290
+ entry.history.splice(0, entry.history.length - format_1.HISTORY_LIMIT);
291
+ }
292
+ entry.value = value;
293
+ entry.type = (0, values_1.typeOf)(value);
294
+ entry.updated = now;
295
+ }
296
+ function plainToNamespace(data, now) {
297
+ const ns = (0, format_1.dict)();
298
+ for (const key of Object.keys(data)) {
299
+ if (key === "")
300
+ continue;
301
+ ns[key] = (0, format_1.toEntry)({ value: data[key] }, now);
302
+ }
303
+ return ns;
304
+ }
305
+ /**
306
+ * Read text that may be UTF-8 or UTF-16 (Windows PowerShell 5's `>` writes UTF-16),
307
+ * with or without a byte order mark.
308
+ */
309
+ function decodeText(buf) {
310
+ if (buf[0] === 0xff && buf[1] === 0xfe)
311
+ return buf.subarray(2).toString("utf16le");
312
+ if (buf[0] === 0xfe && buf[1] === 0xff) {
313
+ const swapped = Buffer.from(buf.subarray(2, buf.length - (buf.length % 2)));
314
+ swapped.swap16();
315
+ return swapped.toString("utf16le");
316
+ }
317
+ if (buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf)
318
+ return buf.subarray(3).toString("utf8");
319
+ return buf.toString("utf8");
320
+ }
@@ -0,0 +1,14 @@
1
+ export type Value = string | number | boolean | null | Value[] | {
2
+ [key: string]: Value;
3
+ };
4
+ export type ValueType = "string" | "number" | "boolean" | "json";
5
+ export declare const VALUE_TYPES: readonly ValueType[];
6
+ export declare function typeOf(value: Value): ValueType;
7
+ /**
8
+ * Turn CLI text into a typed value.
9
+ * Without an explicit type, only unambiguous input is converted, so "007" or "1e5" stay text.
10
+ */
11
+ export declare function parseValue(input: string, type?: ValueType): Value;
12
+ /** Text form for display: strings as-is, everything else as JSON. */
13
+ export declare function formatValue(value: Value, pretty?: boolean): string;
14
+ export declare function isValueType(type: string): type is ValueType;
package/dist/values.js ADDED
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VALUE_TYPES = void 0;
4
+ exports.typeOf = typeOf;
5
+ exports.parseValue = parseValue;
6
+ exports.formatValue = formatValue;
7
+ exports.isValueType = isValueType;
8
+ exports.VALUE_TYPES = ["string", "number", "boolean", "json"];
9
+ function typeOf(value) {
10
+ if (typeof value === "string")
11
+ return "string";
12
+ if (typeof value === "number")
13
+ return "number";
14
+ if (typeof value === "boolean")
15
+ return "boolean";
16
+ return "json";
17
+ }
18
+ /**
19
+ * Turn CLI text into a typed value.
20
+ * Without an explicit type, only unambiguous input is converted, so "007" or "1e5" stay text.
21
+ */
22
+ function parseValue(input, type) {
23
+ switch (type) {
24
+ case "string":
25
+ return input;
26
+ case "number": {
27
+ const n = Number(input);
28
+ if (input.trim() === "" || !Number.isFinite(n))
29
+ throw new Error(`"${input}" is not a number`);
30
+ return n;
31
+ }
32
+ case "boolean":
33
+ if (input === "true")
34
+ return true;
35
+ if (input === "false")
36
+ return false;
37
+ throw new Error(`"${input}" is not a boolean (use true or false)`);
38
+ case "json":
39
+ try {
40
+ return JSON.parse(input);
41
+ }
42
+ catch {
43
+ throw new Error(`"${input}" is not valid JSON`);
44
+ }
45
+ case undefined:
46
+ if (input === "true")
47
+ return true;
48
+ if (input === "false")
49
+ return false;
50
+ if (input.trim() !== "" && String(Number(input)) === input)
51
+ return Number(input);
52
+ if (/^\s*[[{]/.test(input)) {
53
+ try {
54
+ return JSON.parse(input);
55
+ }
56
+ catch {
57
+ // Not JSON after all; keep it as text.
58
+ }
59
+ }
60
+ return input;
61
+ default:
62
+ throw new Error(`Unknown type "${type}". Use one of: ${exports.VALUE_TYPES.join(", ")}`);
63
+ }
64
+ }
65
+ /** Text form for display: strings as-is, everything else as JSON. */
66
+ function formatValue(value, pretty = false) {
67
+ if (typeof value === "string")
68
+ return value;
69
+ return JSON.stringify(value, null, pretty ? 2 : undefined);
70
+ }
71
+ function isValueType(type) {
72
+ return exports.VALUE_TYPES.includes(type);
73
+ }
package/package.json CHANGED
@@ -1,14 +1,18 @@
1
1
  {
2
2
  "name": "local-kv-cli",
3
- "version": "1.0.0",
4
- "description": "A tiny CLI to store key-value pairs locally on your machine",
3
+ "version": "2.0.0",
4
+ "description": "A tiny CLI to store key-value pairs locally: typed values, counters, history, namespaces, per-project stores and env export",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "key-value",
8
8
  "kv",
9
9
  "store",
10
10
  "local",
11
- "storage"
11
+ "storage",
12
+ "counter",
13
+ "env",
14
+ "dotenv",
15
+ "namespace"
12
16
  ],
13
17
  "license": "MIT",
14
18
  "author": "",
@@ -19,14 +23,16 @@
19
23
  "lkv": "dist/cli.js"
20
24
  },
21
25
  "files": [
22
- "dist"
26
+ "dist",
27
+ "CHANGELOG.md"
23
28
  ],
24
29
  "engines": {
25
30
  "node": ">=18"
26
31
  },
27
32
  "scripts": {
28
- "build": "tsc",
29
- "test": "npm run build && node --test",
33
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
34
+ "build": "npm run clean && tsc",
35
+ "test": "npm run build && node --test test/*.test.js",
30
36
  "prepublishOnly": "npm test"
31
37
  },
32
38
  "devDependencies": {