clawpowers 1.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.
- package/.claude-plugin/manifest.json +19 -0
- package/.codex/INSTALL.md +36 -0
- package/.cursor-plugin/manifest.json +21 -0
- package/.opencode/INSTALL.md +52 -0
- package/ARCHITECTURE.md +69 -0
- package/README.md +381 -0
- package/bin/clawpowers.js +390 -0
- package/bin/clawpowers.sh +91 -0
- package/gemini-extension.json +32 -0
- package/hooks/session-start +205 -0
- package/hooks/session-start.cmd +43 -0
- package/hooks/session-start.js +163 -0
- package/package.json +54 -0
- package/runtime/feedback/analyze.js +621 -0
- package/runtime/feedback/analyze.sh +546 -0
- package/runtime/init.js +172 -0
- package/runtime/init.sh +145 -0
- package/runtime/metrics/collector.js +361 -0
- package/runtime/metrics/collector.sh +308 -0
- package/runtime/persistence/store.js +433 -0
- package/runtime/persistence/store.sh +303 -0
- package/skill.json +74 -0
- package/skills/agent-payments/SKILL.md +411 -0
- package/skills/brainstorming/SKILL.md +233 -0
- package/skills/content-pipeline/SKILL.md +282 -0
- package/skills/dispatching-parallel-agents/SKILL.md +305 -0
- package/skills/executing-plans/SKILL.md +255 -0
- package/skills/finishing-a-development-branch/SKILL.md +260 -0
- package/skills/learn-how-to-learn/SKILL.md +235 -0
- package/skills/market-intelligence/SKILL.md +288 -0
- package/skills/prospecting/SKILL.md +313 -0
- package/skills/receiving-code-review/SKILL.md +225 -0
- package/skills/requesting-code-review/SKILL.md +206 -0
- package/skills/security-audit/SKILL.md +308 -0
- package/skills/subagent-driven-development/SKILL.md +244 -0
- package/skills/systematic-debugging/SKILL.md +279 -0
- package/skills/test-driven-development/SKILL.md +299 -0
- package/skills/using-clawpowers/SKILL.md +137 -0
- package/skills/using-git-worktrees/SKILL.md +261 -0
- package/skills/verification-before-completion/SKILL.md +254 -0
- package/skills/writing-plans/SKILL.md +276 -0
- package/skills/writing-skills/SKILL.md +260 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// runtime/persistence/store.js — File-based key-value persistence
|
|
3
|
+
//
|
|
4
|
+
// Stores skill state in ~/.clawpowers/state/ using flat files.
|
|
5
|
+
// Each key maps to a file. Writes are atomic via temp file + rename.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node store.js set <key> <value>
|
|
9
|
+
// node store.js get <key> [default]
|
|
10
|
+
// node store.js delete <key>
|
|
11
|
+
// node store.js list [prefix]
|
|
12
|
+
// node store.js list-values [prefix]
|
|
13
|
+
// node store.js exists <key>
|
|
14
|
+
// node store.js append <key> <value>
|
|
15
|
+
// node store.js incr <key> [amount]
|
|
16
|
+
//
|
|
17
|
+
// Key naming convention: namespace:entity:attribute
|
|
18
|
+
// Keys may contain: [a-zA-Z0-9:_.-]
|
|
19
|
+
// Keys with '/' or '\' or '..' are rejected (path-traversal protection)
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
|
|
26
|
+
// State directory — override parent with CLAWPOWERS_DIR env var for testing
|
|
27
|
+
const STATE_DIR = path.join(
|
|
28
|
+
process.env.CLAWPOWERS_DIR || path.join(os.homedir(), '.clawpowers'),
|
|
29
|
+
'state'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates the state directory if it doesn't already exist.
|
|
34
|
+
* Mode 0o700 ensures the directory is only accessible by the current user.
|
|
35
|
+
* Called before every read/write operation as a cheap guard.
|
|
36
|
+
*/
|
|
37
|
+
function ensureDir() {
|
|
38
|
+
if (!fs.existsSync(STATE_DIR)) {
|
|
39
|
+
fs.mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validates a key before use, preventing empty keys and path-traversal attacks.
|
|
45
|
+
* Keys that pass validation are safe to embed in filenames.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} key - The key to validate.
|
|
48
|
+
* @throws {Error} If the key is empty, contains path separators, or contains '..'.
|
|
49
|
+
*/
|
|
50
|
+
function validateKey(key) {
|
|
51
|
+
if (!key || key.length === 0) {
|
|
52
|
+
throw new Error('key cannot be empty');
|
|
53
|
+
}
|
|
54
|
+
// Reject path separators — they would allow writing outside STATE_DIR
|
|
55
|
+
if (key.includes('/') || key.includes('\\')) {
|
|
56
|
+
throw new Error(`key cannot contain '/' or '\\': ${key}`);
|
|
57
|
+
}
|
|
58
|
+
// Reject '..' segments to prevent directory traversal
|
|
59
|
+
if (key.includes('..')) {
|
|
60
|
+
throw new Error(`key cannot contain '..': ${key}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Converts a colon-separated key to a safe filesystem filename.
|
|
66
|
+
* Colons are replaced with double underscores because ':' is not valid in
|
|
67
|
+
* Windows filenames and can be ambiguous on some filesystems.
|
|
68
|
+
*
|
|
69
|
+
* Example: "execution:my-plan:task_1" → "<STATE_DIR>/execution__my-plan__task_1"
|
|
70
|
+
*
|
|
71
|
+
* @param {string} key - Validated key (colons are safe at this point).
|
|
72
|
+
* @returns {string} Absolute path to the file that stores this key's value.
|
|
73
|
+
*/
|
|
74
|
+
function keyToFile(key) {
|
|
75
|
+
return path.join(STATE_DIR, key.replace(/:/g, '__'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Converts a filesystem filename back to its original colon-separated key.
|
|
80
|
+
* Reverses the transformation applied by keyToFile().
|
|
81
|
+
*
|
|
82
|
+
* @param {string} filename - Base filename (without directory path).
|
|
83
|
+
* @returns {string} Original key with colons restored.
|
|
84
|
+
*/
|
|
85
|
+
function fileToKey(filename) {
|
|
86
|
+
return path.basename(filename).replace(/__/g, ':');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Atomically writes a value to a file using a temp-file-then-rename strategy.
|
|
91
|
+
* This prevents partial writes — readers either see the old value or the new
|
|
92
|
+
* value, never a truncated intermediate state.
|
|
93
|
+
*
|
|
94
|
+
* A trailing newline is appended so the file ends cleanly (Unix convention).
|
|
95
|
+
* Mode 0o600 restricts access to the file owner only.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} filepath - Destination file path.
|
|
98
|
+
* @param {string} value - String value to write.
|
|
99
|
+
*/
|
|
100
|
+
function atomicWrite(filepath, value) {
|
|
101
|
+
// Use PID in temp filename to avoid collisions with concurrent writes
|
|
102
|
+
const tmp = `${filepath}.tmp.${process.pid}`;
|
|
103
|
+
fs.writeFileSync(tmp, value + '\n', { mode: 0o600 });
|
|
104
|
+
// rename() is atomic on POSIX; on Windows it may fail if the destination
|
|
105
|
+
// already exists, but Node.js handles this via a copy+delete fallback
|
|
106
|
+
fs.renameSync(tmp, filepath);
|
|
107
|
+
// Ensure permissions on the final file (rename inherits temp perms on most OSes)
|
|
108
|
+
try { fs.chmodSync(filepath, 0o600); } catch (_) { /* non-fatal on Windows */ }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Sets a key to the given value, overwriting any existing value.
|
|
113
|
+
* Creates the key file if it doesn't exist.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} key - Key to set (validated for safe characters).
|
|
116
|
+
* @param {string} [value=''] - Value to store.
|
|
117
|
+
*/
|
|
118
|
+
function cmdSet(key, value = '') {
|
|
119
|
+
validateKey(key);
|
|
120
|
+
ensureDir();
|
|
121
|
+
atomicWrite(keyToFile(key), value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Gets the value for a key, optionally returning a default if the key is absent.
|
|
126
|
+
*
|
|
127
|
+
* The sentinel `Symbol('NOTSET')` is used internally to distinguish between
|
|
128
|
+
* "caller passed undefined as default" and "caller passed no default argument"
|
|
129
|
+
* — important because `undefined` is a valid JavaScript value.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} key - Key to look up.
|
|
132
|
+
* @param {string} [defaultVal] - Value to return if key doesn't exist.
|
|
133
|
+
* If omitted, throws when the key is not found.
|
|
134
|
+
* @returns {string} The stored value, or defaultVal if key is absent.
|
|
135
|
+
* @throws {Error} If key is not found and no default was provided.
|
|
136
|
+
*/
|
|
137
|
+
function cmdGet(key, defaultVal) {
|
|
138
|
+
const NOTSET = Symbol('NOTSET');
|
|
139
|
+
// arguments.length distinguishes cmdGet(k) from cmdGet(k, undefined)
|
|
140
|
+
const fallback = arguments.length >= 2 ? defaultVal : NOTSET;
|
|
141
|
+
validateKey(key);
|
|
142
|
+
ensureDir();
|
|
143
|
+
|
|
144
|
+
const filepath = keyToFile(key);
|
|
145
|
+
if (fs.existsSync(filepath)) {
|
|
146
|
+
// Strip the trailing newline that atomicWrite appends to every value
|
|
147
|
+
return fs.readFileSync(filepath, 'utf8').replace(/\n$/, '');
|
|
148
|
+
}
|
|
149
|
+
if (fallback !== NOTSET) {
|
|
150
|
+
return fallback;
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`key not found: ${key}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Deletes a key and its associated file.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} key - Key to delete.
|
|
159
|
+
* @returns {string} Confirmation message or "Key not found" message.
|
|
160
|
+
*/
|
|
161
|
+
function cmdDelete(key) {
|
|
162
|
+
validateKey(key);
|
|
163
|
+
const filepath = keyToFile(key);
|
|
164
|
+
if (fs.existsSync(filepath)) {
|
|
165
|
+
fs.unlinkSync(filepath);
|
|
166
|
+
return `Deleted: ${key}`;
|
|
167
|
+
}
|
|
168
|
+
return `Key not found (nothing deleted): ${key}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Lists all keys that start with the given prefix.
|
|
173
|
+
* The prefix uses colon notation (e.g. "execution:my-plan:") which is
|
|
174
|
+
* converted to filename notation before filtering.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} [prefix=''] - Key prefix to filter by; empty string returns all keys.
|
|
177
|
+
* @returns {string[]} Sorted array of matching keys in colon notation.
|
|
178
|
+
*/
|
|
179
|
+
function cmdList(prefix = '') {
|
|
180
|
+
ensureDir();
|
|
181
|
+
// Convert prefix from key format to filename format for comparison
|
|
182
|
+
const filePrefix = prefix.replace(/:/g, '__');
|
|
183
|
+
const entries = fs.readdirSync(STATE_DIR);
|
|
184
|
+
const keys = [];
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
if (entry.startsWith(filePrefix)) {
|
|
187
|
+
const fullPath = path.join(STATE_DIR, entry);
|
|
188
|
+
// Skip directories (shouldn't exist, but be defensive)
|
|
189
|
+
if (fs.statSync(fullPath).isFile()) {
|
|
190
|
+
keys.push(fileToKey(entry));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return keys;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Lists all key=value pairs that start with the given prefix.
|
|
199
|
+
* Each entry is formatted as "key=value" suitable for parsing with `cut -d= -f2`.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} [prefix=''] - Key prefix to filter by.
|
|
202
|
+
* @returns {string[]} Array of "key=value" strings.
|
|
203
|
+
*/
|
|
204
|
+
function cmdListValues(prefix = '') {
|
|
205
|
+
ensureDir();
|
|
206
|
+
const filePrefix = prefix.replace(/:/g, '__');
|
|
207
|
+
const entries = fs.readdirSync(STATE_DIR);
|
|
208
|
+
const pairs = [];
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
if (entry.startsWith(filePrefix)) {
|
|
211
|
+
const fullPath = path.join(STATE_DIR, entry);
|
|
212
|
+
if (fs.statSync(fullPath).isFile()) {
|
|
213
|
+
const key = fileToKey(entry);
|
|
214
|
+
// Strip trailing newline before embedding in "key=value" output
|
|
215
|
+
const value = fs.readFileSync(fullPath, 'utf8').replace(/\n$/, '');
|
|
216
|
+
pairs.push(`${key}=${value}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return pairs;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Checks whether a key exists in the store.
|
|
225
|
+
* Does not read the file contents — only checks for file existence.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} key - Key to check.
|
|
228
|
+
* @returns {boolean} True if the key exists, false otherwise.
|
|
229
|
+
*/
|
|
230
|
+
function cmdExists(key) {
|
|
231
|
+
validateKey(key);
|
|
232
|
+
return fs.existsSync(keyToFile(key));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Appends a value to an existing key, separated by a newline.
|
|
237
|
+
* If the key doesn't exist, creates it with the given value (same as `set`).
|
|
238
|
+
* Useful for maintaining lists, logs, or multi-line notes in a single key.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} key - Key to append to.
|
|
241
|
+
* @param {string} [value=''] - Value to append.
|
|
242
|
+
*/
|
|
243
|
+
function cmdAppend(key, value = '') {
|
|
244
|
+
validateKey(key);
|
|
245
|
+
ensureDir();
|
|
246
|
+
const filepath = keyToFile(key);
|
|
247
|
+
if (fs.existsSync(filepath)) {
|
|
248
|
+
// appendFileSync does not need atomic temp-file since partial appends
|
|
249
|
+
// are acceptable (only new data is added, existing data is intact)
|
|
250
|
+
fs.appendFileSync(filepath, value + '\n', { mode: 0o600 });
|
|
251
|
+
} else {
|
|
252
|
+
// First write: use atomic write to properly create the file
|
|
253
|
+
atomicWrite(filepath, value);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Increments the integer value stored at a key by the given amount.
|
|
259
|
+
* Creates the key with value `amount` if it doesn't exist (treating missing as 0).
|
|
260
|
+
* Rejects non-integer existing values with an error.
|
|
261
|
+
*
|
|
262
|
+
* @param {string} key - Key whose value to increment.
|
|
263
|
+
* @param {number} [amount=1] - Amount to add (can be negative for decrement).
|
|
264
|
+
* @returns {number} The new value after incrementing.
|
|
265
|
+
* @throws {Error} If the existing value is not a valid integer.
|
|
266
|
+
*/
|
|
267
|
+
function cmdIncr(key, amount = 1) {
|
|
268
|
+
validateKey(key);
|
|
269
|
+
ensureDir();
|
|
270
|
+
const filepath = keyToFile(key);
|
|
271
|
+
|
|
272
|
+
let current = 0;
|
|
273
|
+
if (fs.existsSync(filepath)) {
|
|
274
|
+
const raw = fs.readFileSync(filepath, 'utf8').trim();
|
|
275
|
+
// Only allow plain integers — reject floats, whitespace-padded values, etc.
|
|
276
|
+
if (!/^-?[0-9]+$/.test(raw)) {
|
|
277
|
+
throw new Error(`value is not an integer: ${raw}`);
|
|
278
|
+
}
|
|
279
|
+
current = parseInt(raw, 10);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const newVal = current + Number(amount);
|
|
283
|
+
atomicWrite(filepath, String(newVal));
|
|
284
|
+
return newVal;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Prints usage information for the store CLI to stdout.
|
|
289
|
+
*/
|
|
290
|
+
function printUsage() {
|
|
291
|
+
console.log(`Usage: store.js <command> [args]
|
|
292
|
+
|
|
293
|
+
Commands:
|
|
294
|
+
set <key> <value> Set a key-value pair
|
|
295
|
+
get <key> [default] Get value (returns default or error if not found)
|
|
296
|
+
delete <key> Delete a key
|
|
297
|
+
list [prefix] List all keys matching prefix
|
|
298
|
+
list-values [prefix] List key=value pairs matching prefix
|
|
299
|
+
exists <key> Exit 0 if key exists, 1 if not
|
|
300
|
+
append <key> <value> Append value (newline-separated)
|
|
301
|
+
incr <key> [amount] Increment integer value by amount (default: 1)
|
|
302
|
+
|
|
303
|
+
Key format: namespace:entity:attribute (e.g., "execution:auth-plan:task_3:status")
|
|
304
|
+
State stored in: ~/.clawpowers/state/
|
|
305
|
+
|
|
306
|
+
Examples:
|
|
307
|
+
store.js set "execution:my-plan:task_1:status" "complete"
|
|
308
|
+
store.js get "execution:my-plan:task_1:status"
|
|
309
|
+
store.js get "missing-key" "default-value"
|
|
310
|
+
store.js list "execution:my-plan:"
|
|
311
|
+
store.js incr "metrics:session:payment_count"
|
|
312
|
+
store.js exists "execution:my-plan:task_1:status" && echo "exists"`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* CLI dispatch — parses argv and routes to the appropriate command function.
|
|
317
|
+
* Called when this module is run directly (not when require()'d).
|
|
318
|
+
*
|
|
319
|
+
* @param {string[]} argv - Argument array (typically process.argv.slice(2)).
|
|
320
|
+
*/
|
|
321
|
+
function main(argv) {
|
|
322
|
+
const [cmd, ...args] = argv;
|
|
323
|
+
|
|
324
|
+
switch (cmd) {
|
|
325
|
+
case 'set': {
|
|
326
|
+
const [key, value = ''] = args;
|
|
327
|
+
cmdSet(key, value);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case 'get': {
|
|
331
|
+
const [key, def] = args;
|
|
332
|
+
try {
|
|
333
|
+
// Pass default only when it was explicitly provided on the command line
|
|
334
|
+
const val = args.length >= 2 ? cmdGet(key, def) : cmdGet(key);
|
|
335
|
+
console.log(val);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
case 'delete': {
|
|
343
|
+
const [key] = args;
|
|
344
|
+
const msg = cmdDelete(key);
|
|
345
|
+
// Route "Key not found" to stderr to allow shell scripts to detect missing keys
|
|
346
|
+
if (msg.startsWith('Key not found')) {
|
|
347
|
+
process.stderr.write(msg + '\n');
|
|
348
|
+
} else {
|
|
349
|
+
console.log(msg);
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case 'list': {
|
|
354
|
+
const [prefix = ''] = args;
|
|
355
|
+
const keys = cmdList(prefix);
|
|
356
|
+
if (keys.length === 0 && prefix) {
|
|
357
|
+
process.stderr.write(`No keys found with prefix: ${prefix}\n`);
|
|
358
|
+
} else {
|
|
359
|
+
keys.forEach(k => console.log(k));
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
case 'list-values': {
|
|
364
|
+
const [prefix = ''] = args;
|
|
365
|
+
const pairs = cmdListValues(prefix);
|
|
366
|
+
if (pairs.length === 0 && prefix) {
|
|
367
|
+
process.stderr.write(`No keys found with prefix: ${prefix}\n`);
|
|
368
|
+
} else {
|
|
369
|
+
pairs.forEach(p => console.log(p));
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case 'exists': {
|
|
374
|
+
const [key] = args;
|
|
375
|
+
try {
|
|
376
|
+
validateKey(key);
|
|
377
|
+
// Exit 0/1 for shell-script-friendly boolean check
|
|
378
|
+
process.exit(cmdExists(key) ? 0 : 1);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
case 'append': {
|
|
386
|
+
const [key, value = ''] = args;
|
|
387
|
+
try {
|
|
388
|
+
cmdAppend(key, value);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
case 'incr': {
|
|
396
|
+
const [key, amount = '1'] = args;
|
|
397
|
+
try {
|
|
398
|
+
const newVal = cmdIncr(key, parseInt(amount, 10));
|
|
399
|
+
console.log(newVal);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
case 'help':
|
|
407
|
+
case '-h':
|
|
408
|
+
case '--help':
|
|
409
|
+
printUsage();
|
|
410
|
+
break;
|
|
411
|
+
case undefined:
|
|
412
|
+
case '':
|
|
413
|
+
printUsage();
|
|
414
|
+
process.exit(1);
|
|
415
|
+
break;
|
|
416
|
+
default:
|
|
417
|
+
process.stderr.write(`Unknown command: ${cmd}\n`);
|
|
418
|
+
printUsage();
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Guard: only run CLI dispatch when invoked directly, not when require()'d
|
|
424
|
+
if (require.main === module) {
|
|
425
|
+
try {
|
|
426
|
+
main(process.argv.slice(2));
|
|
427
|
+
} catch (err) {
|
|
428
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
module.exports = { cmdSet, cmdGet, cmdDelete, cmdList, cmdListValues, cmdExists, cmdAppend, cmdIncr, STATE_DIR };
|