@yinzuoweia/nis 0.1.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/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # NIS (`@yinzuoweia/nis`)
2
+
3
+ Agent-friendly JSON tree CLI and JS API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm i @yinzuoweia/nis
9
+ ```
10
+
11
+ ## CLI
12
+
13
+ Default file path: `./.nis/tree.json`
14
+
15
+ ```bash
16
+ nis init [--file <path>] [--force]
17
+ nis add --set key=value... [--parent <id>] [--id <id>] [--file <path>]
18
+ nis get <id> [--file <path>]
19
+ nis ls [parentId] [--max <n>] [--file <path>]
20
+ nis update <id> [--set key=value...] [--unset key...] [--file <path>]
21
+ nis delete <id> [--cascade|--no-cascade] [--yes] [--file <path>]
22
+ nis move <id> --to <newParentId> [--file <path>]
23
+ nis find "<query>" [--max <n>] [--sort <field:asc|desc>] [--fields <csv>] [--file <path>]
24
+ nis validate [--file <path>]
25
+ nis upsert --id <id> --set key=value... [--parent <id>] [--file <path>]
26
+ nis bulk --ops-file <json> [--atomic|--no-atomic] [--file <path>]
27
+ nis snapshot create [--name <name>] [--file <path>]
28
+ nis snapshot restore <snapshotId> [--file <path>]
29
+ ```
30
+
31
+ ### Natural aliases (`spark`)
32
+
33
+ ```bash
34
+ nis spark search "newer_than:7d tag:idea" --max 10
35
+ nis spark add "summary:idea description:detail" under root
36
+ nis spark delete <id> --yes
37
+ ```
38
+
39
+ ## Query DSL (v1)
40
+
41
+ - Full text term: `transformer`
42
+ - Field filter: `tag:idea`
43
+ - Comparators: `score>=0.9`, `updated_at>2026-03-01`
44
+ - Relative time: `newer_than:7d`, `older_than:30d`
45
+ - Multiple conditions are AND.
46
+
47
+ ## JS API
48
+
49
+ ```ts
50
+ import {
51
+ initTree,
52
+ addNode,
53
+ updateNode,
54
+ deleteNode,
55
+ moveNode,
56
+ findNodes,
57
+ validateTree,
58
+ applyBulk,
59
+ createSnapshot,
60
+ restoreSnapshot,
61
+ } from '@yinzuoweia/nis';
62
+ ```
63
+
64
+ ## Output Contract
65
+
66
+ CLI always writes machine-readable JSON:
67
+
68
+ - success: `{"ok": true, "action": "...", "file": "...", "result": ..., "warnings": []}`
69
+ - error: `{"ok": false, "action": "...", "error": {"code": "...", "message": "...", "hint": "..."}}`
70
+
71
+ ## Notes
72
+
73
+ - Root node is fixed as `root` and immutable.
74
+ - Reserved fields: `id,parent,children,created_at,updated_at`.
75
+ - Writes are protected by file lock + atomic rename.
76
+ - Snapshot retention default is `20` and can be configured with `NIS_SNAPSHOT_KEEP`.
package/dist/api.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { addNode, createSnapshot, deleteNode, getNode, initTree, listChildren, moveNode, parseSetPairs, parseSparkExpression, restoreSnapshot, updateNode, upsertNode, validateTree } from './core.js';
2
+ import type { AddInput, BulkInput, DeleteInput, FindInput, InitOptions, MutateOptions, SnapshotInfo, UpdateInput } from './core.js';
3
+ export { initTree, addNode, getNode, listChildren, updateNode, deleteNode, moveNode, validateTree, upsertNode, createSnapshot, restoreSnapshot, parseSetPairs, parseSparkExpression };
4
+ export type { InitOptions, AddInput, UpdateInput, DeleteInput, FindInput, BulkInput, SnapshotInfo, MutateOptions };
5
+ export declare function findNodes(filePath: string | undefined, query: string, options?: Omit<FindInput, 'query'>): Promise<Array<Record<string, unknown>>>;
6
+ export declare function applyBulk(filePath: string | undefined, ops: Array<Record<string, unknown>>, options?: Omit<MutateOptions, 'autoSnapshot'> & {
7
+ atomic?: boolean;
8
+ }): Promise<{
9
+ applied: number;
10
+ results: Array<Record<string, unknown>>;
11
+ }>;
12
+ export declare function applyBulkFromOpsFile(filePath: string | undefined, input: BulkInput, options?: MutateOptions): Promise<{
13
+ applied: number;
14
+ results: Array<Record<string, unknown>>;
15
+ }>;
package/dist/api.js ADDED
@@ -0,0 +1,29 @@
1
+ import { rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { addNode, applyBulkFromFile, createSnapshot, deleteNode, findNodesInternal, getNode, initTree, listChildren, moveNode, parseSetPairs, parseSparkExpression, restoreSnapshot, updateNode, upsertNode, validateTree } from './core.js';
5
+ export { initTree, addNode, getNode, listChildren, updateNode, deleteNode, moveNode, validateTree, upsertNode, createSnapshot, restoreSnapshot, parseSetPairs, parseSparkExpression };
6
+ export async function findNodes(filePath, query, options = {}) {
7
+ return findNodesInternal(filePath, {
8
+ query,
9
+ ...options
10
+ });
11
+ }
12
+ export async function applyBulk(filePath, ops, options = {}) {
13
+ const tempOpsFile = join(tmpdir(), `.nis-bulk-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
14
+ await writeFile(tempOpsFile, JSON.stringify(ops), 'utf-8');
15
+ try {
16
+ return await applyBulkFromFile(filePath, {
17
+ opsFile: tempOpsFile,
18
+ atomic: options.atomic
19
+ }, {
20
+ snapshotKeep: options.snapshotKeep
21
+ });
22
+ }
23
+ finally {
24
+ await rm(tempOpsFile, { force: true });
25
+ }
26
+ }
27
+ export async function applyBulkFromOpsFile(filePath, input, options = {}) {
28
+ return applyBulkFromFile(filePath, input, options);
29
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { CliError } from './errors.js';
4
+ import { addNode, applyBulkFromOpsFile, createSnapshot, deleteNode, findNodes, getNode, initTree, listChildren, moveNode, parseSetPairs, parseSparkExpression, restoreSnapshot, updateNode, upsertNode, validateTree } from './api.js';
5
+ const EXIT_MAP = {
6
+ FILE_NOT_FOUND: 2,
7
+ SCHEMA_INVALID: 3,
8
+ NODE_NOT_FOUND: 4,
9
+ NODE_ID_CONFLICT: 5,
10
+ ROOT_IMMUTABLE: 6,
11
+ DELETE_CONFIRM_REQUIRED: 7,
12
+ CYCLE_DETECTED: 8,
13
+ LOCK_TIMEOUT: 9
14
+ };
15
+ function outputSuccess(action, file, result, warnings = []) {
16
+ const payload = {
17
+ ok: true,
18
+ action,
19
+ file,
20
+ result,
21
+ warnings
22
+ };
23
+ console.log(JSON.stringify(payload));
24
+ }
25
+ function outputError(action, err) {
26
+ if (err instanceof CliError) {
27
+ const payload = {
28
+ ok: false,
29
+ action,
30
+ error: {
31
+ code: err.code,
32
+ message: err.message,
33
+ hint: err.hint
34
+ }
35
+ };
36
+ console.error(JSON.stringify(payload));
37
+ process.exit(EXIT_MAP[err.code] ?? 1);
38
+ }
39
+ const message = err instanceof Error ? err.message : String(err);
40
+ const payload = {
41
+ ok: false,
42
+ action,
43
+ error: {
44
+ code: 'UNKNOWN',
45
+ message
46
+ }
47
+ };
48
+ console.error(JSON.stringify(payload));
49
+ process.exit(1);
50
+ }
51
+ const program = new Command();
52
+ program.name('nis').description('NIS CLI for JSON tree operations').version('0.1.0');
53
+ program
54
+ .command('init')
55
+ .option('--file <path>')
56
+ .option('--force', 'overwrite existing tree')
57
+ .action(async (opts) => {
58
+ try {
59
+ const result = await initTree(opts.file, { force: Boolean(opts.force) });
60
+ outputSuccess('init', result.file, result);
61
+ }
62
+ catch (err) {
63
+ outputError('init', err);
64
+ }
65
+ });
66
+ program
67
+ .command('add')
68
+ .requiredOption('--set <pair...>', 'key=value pairs')
69
+ .option('--file <path>')
70
+ .option('--parent <id>')
71
+ .option('--id <id>')
72
+ .action(async (opts) => {
73
+ try {
74
+ const result = await addNode(opts.file, {
75
+ parent: opts.parent,
76
+ id: opts.id,
77
+ set: parseSetPairs(opts.set)
78
+ });
79
+ outputSuccess('add', opts.file ?? '.nis/tree.json', result);
80
+ }
81
+ catch (err) {
82
+ outputError('add', err);
83
+ }
84
+ });
85
+ program
86
+ .command('get')
87
+ .argument('<id>')
88
+ .option('--file <path>')
89
+ .action(async (id, opts) => {
90
+ try {
91
+ const result = await getNode(opts.file, id);
92
+ outputSuccess('get', opts.file ?? '.nis/tree.json', result);
93
+ }
94
+ catch (err) {
95
+ outputError('get', err);
96
+ }
97
+ });
98
+ program
99
+ .command('ls')
100
+ .argument('[parentId]')
101
+ .option('--file <path>')
102
+ .option('--max <n>', 'limit', (v) => Number(v))
103
+ .action(async (parentId, opts) => {
104
+ try {
105
+ const result = await listChildren(opts.file, parentId, opts.max);
106
+ outputSuccess('ls', opts.file ?? '.nis/tree.json', result);
107
+ }
108
+ catch (err) {
109
+ outputError('ls', err);
110
+ }
111
+ });
112
+ program
113
+ .command('update')
114
+ .argument('<id>')
115
+ .option('--file <path>')
116
+ .option('--set <pair...>')
117
+ .option('--unset <key...>')
118
+ .action(async (id, opts) => {
119
+ try {
120
+ const result = await updateNode(opts.file, id, {
121
+ set: parseSetPairs(opts.set ?? []),
122
+ unset: opts.unset ?? []
123
+ });
124
+ outputSuccess('update', opts.file ?? '.nis/tree.json', result);
125
+ }
126
+ catch (err) {
127
+ outputError('update', err);
128
+ }
129
+ });
130
+ program
131
+ .command('delete')
132
+ .argument('<id>')
133
+ .option('--file <path>')
134
+ .option('--cascade', 'cascade delete')
135
+ .option('--no-cascade', 'disable cascade delete')
136
+ .option('--yes', 'confirm delete')
137
+ .action(async (id, opts) => {
138
+ try {
139
+ const result = await deleteNode(opts.file, id, { cascade: opts.cascade !== false, yes: Boolean(opts.yes) });
140
+ outputSuccess(opts.yes ? 'delete' : 'delete_preview', opts.file ?? '.nis/tree.json', result);
141
+ }
142
+ catch (err) {
143
+ outputError('delete', err);
144
+ }
145
+ });
146
+ program
147
+ .command('move')
148
+ .argument('<id>')
149
+ .requiredOption('--to <id>')
150
+ .option('--file <path>')
151
+ .action(async (id, opts) => {
152
+ try {
153
+ const result = await moveNode(opts.file, id, opts.to);
154
+ outputSuccess('move', opts.file ?? '.nis/tree.json', result);
155
+ }
156
+ catch (err) {
157
+ outputError('move', err);
158
+ }
159
+ });
160
+ program
161
+ .command('find')
162
+ .argument('<query>')
163
+ .option('--file <path>')
164
+ .option('--max <n>', 'limit', (v) => Number(v))
165
+ .option('--sort <spec>')
166
+ .option('--fields <csv>')
167
+ .action(async (query, opts) => {
168
+ try {
169
+ const fields = typeof opts.fields === 'string' ? opts.fields.split(',').map((f) => f.trim()).filter(Boolean) : undefined;
170
+ const result = await findNodes(opts.file, query, {
171
+ max: opts.max,
172
+ sort: opts.sort,
173
+ fields
174
+ });
175
+ outputSuccess('find', opts.file ?? '.nis/tree.json', result);
176
+ }
177
+ catch (err) {
178
+ outputError('find', err);
179
+ }
180
+ });
181
+ program
182
+ .command('validate')
183
+ .option('--file <path>')
184
+ .action(async (opts) => {
185
+ try {
186
+ const result = await validateTree(opts.file);
187
+ outputSuccess('validate', opts.file ?? '.nis/tree.json', result, result.warnings);
188
+ }
189
+ catch (err) {
190
+ outputError('validate', err);
191
+ }
192
+ });
193
+ program
194
+ .command('upsert')
195
+ .requiredOption('--id <id>')
196
+ .requiredOption('--set <pair...>')
197
+ .option('--parent <id>')
198
+ .option('--file <path>')
199
+ .action(async (opts) => {
200
+ try {
201
+ const result = await upsertNode(opts.file, {
202
+ id: opts.id,
203
+ parent: opts.parent,
204
+ set: parseSetPairs(opts.set)
205
+ });
206
+ outputSuccess('upsert', opts.file ?? '.nis/tree.json', result);
207
+ }
208
+ catch (err) {
209
+ outputError('upsert', err);
210
+ }
211
+ });
212
+ program
213
+ .command('bulk')
214
+ .requiredOption('--ops-file <path>')
215
+ .option('--file <path>')
216
+ .option('--atomic', 'enable atomic rollback')
217
+ .option('--no-atomic', 'disable atomic rollback')
218
+ .action(async (opts) => {
219
+ try {
220
+ const result = await applyBulkFromOpsFile(opts.file, { opsFile: opts.opsFile, atomic: opts.atomic !== false });
221
+ outputSuccess('bulk', opts.file ?? '.nis/tree.json', result);
222
+ }
223
+ catch (err) {
224
+ outputError('bulk', err);
225
+ }
226
+ });
227
+ const snapshot = program.command('snapshot');
228
+ snapshot
229
+ .command('create')
230
+ .option('--file <path>')
231
+ .option('--name <name>')
232
+ .action(async (opts) => {
233
+ try {
234
+ const result = await createSnapshot(opts.file, opts.name);
235
+ outputSuccess('snapshot_create', opts.file ?? '.nis/tree.json', result);
236
+ }
237
+ catch (err) {
238
+ outputError('snapshot_create', err);
239
+ }
240
+ });
241
+ snapshot
242
+ .command('restore')
243
+ .argument('<snapshotId>')
244
+ .option('--file <path>')
245
+ .action(async (snapshotId, opts) => {
246
+ try {
247
+ const result = await restoreSnapshot(opts.file, snapshotId);
248
+ outputSuccess('snapshot_restore', opts.file ?? '.nis/tree.json', result);
249
+ }
250
+ catch (err) {
251
+ outputError('snapshot_restore', err);
252
+ }
253
+ });
254
+ const spark = program.command('spark').description('natural-language style aliases');
255
+ spark
256
+ .command('search')
257
+ .argument('<query>')
258
+ .option('--file <path>')
259
+ .option('--max <n>', 'limit', (v) => Number(v))
260
+ .option('--sort <spec>')
261
+ .action(async (query, opts) => {
262
+ try {
263
+ const result = await findNodes(opts.file, query, {
264
+ max: opts.max,
265
+ sort: opts.sort
266
+ });
267
+ outputSuccess('find', opts.file ?? '.nis/tree.json', result);
268
+ }
269
+ catch (err) {
270
+ outputError('find', err);
271
+ }
272
+ });
273
+ spark
274
+ .command('add')
275
+ .argument('<expression>')
276
+ .argument('[underKeyword]')
277
+ .argument('[parentId]')
278
+ .option('--file <path>')
279
+ .option('--id <id>')
280
+ .action(async (expression, underKeyword, parentId, opts) => {
281
+ try {
282
+ if (underKeyword && underKeyword !== 'under') {
283
+ throw new CliError('SCHEMA_INVALID', `expected keyword 'under', got '${underKeyword}'`);
284
+ }
285
+ const result = await addNode(opts.file, {
286
+ parent: parentId ?? 'root',
287
+ id: opts.id,
288
+ set: parseSparkExpression(expression)
289
+ });
290
+ outputSuccess('add', opts.file ?? '.nis/tree.json', result);
291
+ }
292
+ catch (err) {
293
+ outputError('add', err);
294
+ }
295
+ });
296
+ spark
297
+ .command('delete')
298
+ .argument('<id>')
299
+ .option('--file <path>')
300
+ .option('--yes')
301
+ .action(async (id, opts) => {
302
+ try {
303
+ const result = await deleteNode(opts.file, id, {
304
+ cascade: true,
305
+ yes: Boolean(opts.yes)
306
+ });
307
+ outputSuccess(opts.yes ? 'delete' : 'delete_preview', opts.file ?? '.nis/tree.json', result);
308
+ }
309
+ catch (err) {
310
+ outputError('delete', err);
311
+ }
312
+ });
313
+ await program.parseAsync(process.argv);
package/dist/core.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { type TreeNode } from './types.js';
2
+ export type MutateOptions = {
3
+ autoSnapshot?: boolean;
4
+ snapshotKeep?: number;
5
+ };
6
+ export type InitOptions = {
7
+ force?: boolean;
8
+ };
9
+ export type AddInput = {
10
+ parent?: string;
11
+ id?: string;
12
+ set: Record<string, unknown>;
13
+ };
14
+ export type UpdateInput = {
15
+ set?: Record<string, unknown>;
16
+ unset?: string[];
17
+ };
18
+ export type DeleteInput = {
19
+ cascade?: boolean;
20
+ yes?: boolean;
21
+ };
22
+ export type FindInput = {
23
+ query: string;
24
+ max?: number;
25
+ sort?: string;
26
+ fields?: string[];
27
+ };
28
+ export type BulkInput = {
29
+ opsFile: string;
30
+ atomic?: boolean;
31
+ };
32
+ export type SnapshotInfo = {
33
+ snapshot_id: string;
34
+ path: string;
35
+ };
36
+ export declare function initTree(filePathRaw?: string, options?: InitOptions): Promise<{
37
+ file: string;
38
+ root_id: string;
39
+ }>;
40
+ export declare function addNode(filePathRaw: string | undefined, input: AddInput, options?: MutateOptions): Promise<{
41
+ id: string;
42
+ parent: string;
43
+ }>;
44
+ export declare function getNode(filePathRaw: string | undefined, nodeId: string): Promise<TreeNode>;
45
+ export declare function listChildren(filePathRaw: string | undefined, parentId?: string, max?: number): Promise<TreeNode[]>;
46
+ export declare function updateNode(filePathRaw: string | undefined, nodeId: string, input: UpdateInput, options?: MutateOptions): Promise<TreeNode>;
47
+ export declare function deleteNode(filePathRaw: string | undefined, nodeId: string, input: DeleteInput, options?: MutateOptions): Promise<{
48
+ requires_confirmation?: boolean;
49
+ count?: number;
50
+ ids?: string[];
51
+ deleted_ids?: string[];
52
+ }>;
53
+ export declare function moveNode(filePathRaw: string | undefined, nodeId: string, newParentId: string, options?: MutateOptions): Promise<{
54
+ id: string;
55
+ from: string | null;
56
+ to: string;
57
+ }>;
58
+ export declare function findNodesInternal(filePathRaw: string | undefined, input: FindInput): Promise<Array<Record<string, unknown>>>;
59
+ export declare function validateTree(filePathRaw?: string): Promise<{
60
+ valid: boolean;
61
+ errors: string[];
62
+ warnings: string[];
63
+ }>;
64
+ export declare function upsertNode(filePathRaw: string | undefined, input: {
65
+ id: string;
66
+ parent?: string;
67
+ set: Record<string, unknown>;
68
+ }, options?: MutateOptions): Promise<{
69
+ id: string;
70
+ created: boolean;
71
+ parent: string;
72
+ }>;
73
+ export declare function applyBulkFromFile(filePathRaw: string | undefined, input: BulkInput, options?: MutateOptions): Promise<{
74
+ applied: number;
75
+ results: Array<Record<string, unknown>>;
76
+ }>;
77
+ export declare function createSnapshot(filePathRaw?: string, name?: string): Promise<SnapshotInfo>;
78
+ export declare function restoreSnapshot(filePathRaw: string | undefined, snapshotId: string): Promise<{
79
+ restored: string;
80
+ }>;
81
+ export declare function listSnapshots(filePathRaw?: string): Promise<string[]>;
82
+ export declare function parseSetPairs(pairs?: string[]): Record<string, unknown>;
83
+ export declare function parseSparkExpression(expression: string): Record<string, unknown>;
84
+ export declare function snapshotIdFromPath(pathValue: string): string;