@mak-98/dirvi-cli 0.6.0 → 0.7.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/domain/app-api.d.ts +7 -3
- package/dist/domain/app-api.d.ts.map +1 -1
- package/dist/domain/posix-tree-node.d.ts +33 -0
- package/dist/domain/posix-tree-node.d.ts.map +1 -0
- package/dist/domain/posix-tree-node.js +53 -0
- package/dist/domain/unix-path.d.ts +6 -0
- package/dist/domain/unix-path.d.ts.map +1 -0
- package/dist/domain/unix-path.js +11 -0
- package/dist/domain/view-api.d.ts +11 -0
- package/dist/domain/view-api.d.ts.map +1 -0
- package/dist/index.js +2 -4
- package/dist/infrastructure/create-file-view-api.d.ts +30 -0
- package/dist/infrastructure/create-file-view-api.d.ts.map +1 -0
- package/dist/infrastructure/create-file-view-api.js +154 -0
- package/dist/infrastructure/get-dir-entries.d.ts +3 -2
- package/dist/infrastructure/get-dir-entries.d.ts.map +1 -1
- package/dist/infrastructure/get-dir-entries.js +2 -1
- package/dist/infrastructure/get-unix-abs-path.d.ts +1 -1
- package/dist/infrastructure/get-unix-abs-path.d.ts.map +1 -1
- package/dist/infrastructure/get-unix-abs-path.js +1 -1
- package/dist/infrastructure/load-posix-app-api.d.ts +15 -2
- package/dist/infrastructure/load-posix-app-api.d.ts.map +1 -1
- package/dist/infrastructure/load-posix-app-api.js +82 -2
- package/dist/infrastructure/view-name-schema.d.ts +18 -0
- package/dist/infrastructure/view-name-schema.d.ts.map +1 -0
- package/dist/infrastructure/view-name-schema.js +68 -0
- package/dist/ui/App.d.ts +3 -3
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +46 -0
- package/dist/ui/AppShell.d.ts +1 -1
- package/dist/ui/AppShell.d.ts.map +1 -1
- package/dist/ui/AppShell.js +1 -1
- package/dist/ui/LoadingApp.d.ts +3 -3
- package/dist/ui/LoadingApp.d.ts.map +1 -1
- package/dist/ui/LoadingApp.js +1 -1
- package/package.json +5 -3
- package/dist/domain/event-message.d.ts +0 -12
- package/dist/domain/event-message.d.ts.map +0 -1
- package/dist/infrastructure/get-dir-lazy-entries.d.ts +0 -3
- package/dist/infrastructure/get-dir-lazy-entries.d.ts.map +0 -1
- package/dist/infrastructure/get-dir-lazy-entries.js +0 -33
- /package/dist/domain/{event-message.js → view-api.js} +0 -0
package/dist/domain/app-api.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { CursorApi, FoldNodeApi, FoldNodeService, NavNodeApi, SerializableKey, StateApi, TreeNode, TreeNodeApi } from 'dirvi-lib';
|
|
2
|
-
|
|
1
|
+
import { CursorApi, FoldNodeApi, FoldNodeService, NavNodeApi, SerializableKey, State, StateApi, TreeNode, TreeNodeApi } from 'dirvi-lib';
|
|
2
|
+
import { ViewApi } from './view-api.js';
|
|
3
|
+
export type AppApi<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, ViewKey = string> = {
|
|
4
|
+
appId: string;
|
|
3
5
|
/**
|
|
4
|
-
* Name displayed to distinguish
|
|
6
|
+
* Name displayed to distinguish app instance.
|
|
5
7
|
*/
|
|
6
8
|
name: string;
|
|
7
9
|
/**
|
|
@@ -35,6 +37,8 @@ export type AppApi<Id extends SerializableKey, BufferNode extends TreeNode<Id, B
|
|
|
35
37
|
cursorApi: CursorApi<Id>;
|
|
36
38
|
stateApi: StateApi<Id, BufferNode>;
|
|
37
39
|
navNodeApi: NavNodeApi<Id, BufferNode>;
|
|
40
|
+
viewKey: ViewKey;
|
|
41
|
+
viewApi: ViewApi<State<Id, BufferNode>, ViewKey>;
|
|
38
42
|
};
|
|
39
43
|
export declare function createAppApis<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>(): {
|
|
40
44
|
treeNodeApi: TreeNodeApi<Id, BufferNode>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-api.d.ts","sourceRoot":"","sources":["../../src/domain/app-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,SAAS,EACT,WAAW,EACX,eAAe,EACf,UAAU,EACV,eAAe,EACf,QAAQ,EACR,QAAQ,EACR,WAAW,EACZ,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"app-api.d.ts","sourceRoot":"","sources":["../../src/domain/app-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,SAAS,EACT,WAAW,EACX,eAAe,EACf,UAAU,EACV,eAAe,EACf,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,WAAW,EACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,MAAM,MAAM,MAAM,CAChB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,OAAO,GAAG,MAAM,IACd;IAIF,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAE3B;;;;OAIG;IACH,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAEpD;;;;;OAKG;IACH,iBAAiB,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;IAExD,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAEzC;;OAEG;IACH,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;IAE7B;;;OAGG;IACH,eAAe,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC;IAErC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IAEzB,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAEnC,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAEvC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC;CAClD,CAAC;AAEF,wBAAgB,aAAa,CAC3B,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,KACxC;IACH,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IACzC,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;IAC7B,eAAe,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC;IACrC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC,CAAC;IACzB,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IACnC,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;CACxC,CAkCA"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { UnixPath } from './unix-path.js';
|
|
3
|
+
import { Cursor, FoldNode, FoldNodeRoot, NavNode, State } from 'dirvi-lib';
|
|
4
|
+
export declare const PosixNameSchema: z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodString>;
|
|
5
|
+
export type PosixName = z.output<typeof PosixNameSchema>;
|
|
6
|
+
export declare const PosixTreeNodeSchema: z.ZodType<PosixTreeNode>;
|
|
7
|
+
export type PosixTreeNode = {
|
|
8
|
+
kind: 'file';
|
|
9
|
+
id: PosixName;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'symlink';
|
|
12
|
+
id: PosixName;
|
|
13
|
+
target: UnixPath;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'directory';
|
|
16
|
+
id: PosixName;
|
|
17
|
+
children: PosixTreeNode[] | null;
|
|
18
|
+
};
|
|
19
|
+
export declare const PosixTreeNodeApi: import("dirvi-lib").TreeNodeApi<string, PosixTreeNode>;
|
|
20
|
+
export declare const PosixFoldNodeSchema: z.ZodType<PosixFoldNode>;
|
|
21
|
+
export declare const PosixFoldNodeRootSchema: z.ZodType<PosixFoldNodeRoot>;
|
|
22
|
+
export type PosixFoldNode = FoldNode<PosixName>;
|
|
23
|
+
export type PosixFoldNodeRoot = FoldNodeRoot<PosixName>;
|
|
24
|
+
export declare const PosixFoldNodeApi: import("dirvi-lib").FoldNodeApi<string>;
|
|
25
|
+
export declare const PosixCursorSchema: z.ZodType<PosixCursor>;
|
|
26
|
+
export type PosixCursor = Cursor<PosixName>;
|
|
27
|
+
export declare const PosixCursorApi: import("dirvi-lib").CursorApi<string>;
|
|
28
|
+
export type PosixNavNode = NavNode<PosixName, PosixTreeNode>;
|
|
29
|
+
export declare const PosixNavApi: import("dirvi-lib").NavNodeApi<string, PosixTreeNode>;
|
|
30
|
+
export declare const PosixStateSchema: z.ZodType<PosixState>;
|
|
31
|
+
export type PosixState = State<PosixName, PosixTreeNode>;
|
|
32
|
+
export declare const PosixStateApi: import("dirvi-lib").StateApi<string, PosixTreeNode>;
|
|
33
|
+
//# sourceMappingURL=posix-tree-node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"posix-tree-node.d.ts","sourceRoot":"","sources":["../../src/domain/posix-tree-node.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAkB,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAUL,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,OAAO,EAEP,KAAK,EAEN,MAAM,WAAW,CAAC;AAQnB,eAAO,MAAM,eAAe,yEAU3B,CAAC;AAGF,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAUzD,eAAO,MAAM,mBAAmB,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAsBvD,CAAC;AAGH,MAAM,MAAM,aAAa,GACrB;IACE,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,SAAS,CAAC;CACf,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,EAAE,QAAQ,CAAC;CAClB,GACD;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,EAAE,EAAE,SAAS,CAAC;IACd,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;CAClC,CAAC;AAQN,eAAO,MAAM,gBAAgB,wDAAgD,CAAC;AAQ9E,eAAO,MAAM,mBAAmB,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAkB,CAAC;AAC5E,eAAO,MAAM,uBAAuB,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAC7C,CAAC;AAGrB,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;AAChD,MAAM,MAAM,iBAAiB,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;AAGxD,eAAO,MAAM,gBAAgB,yCAAiC,CAAC;AAK/D,eAAO,MAAM,iBAAiB,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAChB,CAAC;AAGtC,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;AAE5C,eAAO,MAAM,cAAc,uCAA+B,CAAC;AAG3D,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;AAE7D,eAAO,MAAM,WAAW,uDAIvB,CAAC;AAKF,eAAO,MAAM,gBAAgB,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAIlD,CAAC;AAGF,MAAM,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;AAGzD,eAAO,MAAM,aAAa,qDAKzB,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { UnixPathSchema } from './unix-path.js';
|
|
3
|
+
import { createCursorApi, createCursorSchema, createFoldNodeApi, createFoldNodeSchemas, createNavNodeApi, createStateApi, createStateSchema, createTreeNodeApi, createTreeNodeSchemas, serializableKeySchema, } from 'dirvi-lib';
|
|
4
|
+
// --- PosixName extends PropertyKey ---
|
|
5
|
+
// Schema
|
|
6
|
+
export const PosixNameSchema = serializableKeySchema.pipe(z
|
|
7
|
+
.string()
|
|
8
|
+
.min(1, 'Entry name cannot be empty.')
|
|
9
|
+
.refine((value) => !value.includes('/'), {
|
|
10
|
+
message: "Entry name must not contain '/'.",
|
|
11
|
+
})
|
|
12
|
+
.refine((value) => value !== '.' && value !== '..', {
|
|
13
|
+
message: "Entry name cannot be '.' or '..'.",
|
|
14
|
+
}));
|
|
15
|
+
// --- PosixTreeNode extends TreeNode ---
|
|
16
|
+
// Schema
|
|
17
|
+
export const PosixTreeNodeSchema = z.lazy(() => {
|
|
18
|
+
const { leafSchema, closedBranchSchema, openBranchSchema } = createTreeNodeSchemas(PosixNameSchema, PosixTreeNodeSchema);
|
|
19
|
+
return z.union([
|
|
20
|
+
leafSchema.extend({
|
|
21
|
+
kind: z.literal('file'),
|
|
22
|
+
}),
|
|
23
|
+
leafSchema.extend({
|
|
24
|
+
kind: z.literal('symlink'),
|
|
25
|
+
target: UnixPathSchema,
|
|
26
|
+
}),
|
|
27
|
+
closedBranchSchema.extend({
|
|
28
|
+
kind: z.literal('directory'),
|
|
29
|
+
}),
|
|
30
|
+
openBranchSchema.extend({
|
|
31
|
+
kind: z.literal('directory'),
|
|
32
|
+
}),
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
// API
|
|
36
|
+
export const PosixTreeNodeApi = createTreeNodeApi();
|
|
37
|
+
// --- PosixFoldNode ---
|
|
38
|
+
// Schema
|
|
39
|
+
const { foldNodeSchema, foldNodeRootSchema } = createFoldNodeSchemas(PosixNameSchema);
|
|
40
|
+
export const PosixFoldNodeSchema = foldNodeSchema;
|
|
41
|
+
export const PosixFoldNodeRootSchema = foldNodeRootSchema;
|
|
42
|
+
// API
|
|
43
|
+
export const PosixFoldNodeApi = createFoldNodeApi();
|
|
44
|
+
// --- PosixCursor ---
|
|
45
|
+
// Schema
|
|
46
|
+
export const PosixCursorSchema = createCursorSchema(PosixNameSchema);
|
|
47
|
+
export const PosixCursorApi = createCursorApi();
|
|
48
|
+
export const PosixNavApi = createNavNodeApi(PosixTreeNodeApi, PosixFoldNodeApi, PosixCursorApi);
|
|
49
|
+
// --- PosixState ---
|
|
50
|
+
// Schema
|
|
51
|
+
export const PosixStateSchema = createStateSchema(PosixTreeNodeSchema, PosixFoldNodeRootSchema, PosixCursorSchema);
|
|
52
|
+
// API
|
|
53
|
+
export const PosixStateApi = createStateApi(PosixTreeNodeApi, PosixFoldNodeApi, PosixCursorApi, PosixNavApi);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const UnixPathSchema: z.ZodString;
|
|
3
|
+
export type UnixPath = z.output<typeof UnixPathSchema>;
|
|
4
|
+
export declare const UnixAbsolutePathSchema: z.ZodString;
|
|
5
|
+
export type UnixAbsolutePath = z.output<typeof UnixAbsolutePathSchema>;
|
|
6
|
+
//# sourceMappingURL=unix-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unix-path.d.ts","sourceRoot":"","sources":["../../src/domain/unix-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,cAAc,aAKvB,CAAC;AAEL,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAEvD,eAAO,MAAM,sBAAsB,aAKlC,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// UNIX PATHS
|
|
3
|
+
export const UnixPathSchema = z
|
|
4
|
+
.string()
|
|
5
|
+
.min(1)
|
|
6
|
+
.refine((value) => !value.includes('\0'), {
|
|
7
|
+
message: 'Unix paths must not contain null bytes',
|
|
8
|
+
});
|
|
9
|
+
export const UnixAbsolutePathSchema = UnixPathSchema.refine((value) => value.startsWith('/'), {
|
|
10
|
+
message: 'Path must be an absolute Unix path',
|
|
11
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ViewSummary = {
|
|
2
|
+
name: string;
|
|
3
|
+
updatedAt: string;
|
|
4
|
+
};
|
|
5
|
+
export type ViewApi<State, Key> = {
|
|
6
|
+
save: (key: Key, name: string, state: State) => Promise<void>;
|
|
7
|
+
load: (key: Key, name: string) => Promise<State | undefined>;
|
|
8
|
+
list: (key: Key) => Promise<readonly ViewSummary[]>;
|
|
9
|
+
remove: (key: Key, name: string) => Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=view-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-api.d.ts","sourceRoot":"","sources":["../../src/domain/view-api.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,OAAO,CAAC,KAAK,EAAE,GAAG,IAAI;IAChC,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IAC7D,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAC;IACpD,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ process.on('exit', restoreTerminal);
|
|
|
28
28
|
program
|
|
29
29
|
.name('direx')
|
|
30
30
|
.description('View and manage directories.')
|
|
31
|
-
.version('0.
|
|
31
|
+
.version('0.7.0')
|
|
32
32
|
.helpOption('--help')
|
|
33
33
|
.option('-d, --directory <path>', 'Directory to browse')
|
|
34
34
|
.action(async () => {
|
|
@@ -59,9 +59,7 @@ program
|
|
|
59
59
|
try {
|
|
60
60
|
const app = render(_jsx(AppShell, { ...(options.directory === undefined
|
|
61
61
|
? {}
|
|
62
|
-
: { directory: options.directory }), ...(stdoutIsInteractive
|
|
63
|
-
? {}
|
|
64
|
-
: { stdout }), clipboard: clipboard, onError: (error) => {
|
|
62
|
+
: { directory: options.directory }), ...(stdoutIsInteractive ? {} : { stdout }), clipboard: clipboard, onError: (error) => {
|
|
65
63
|
appError = error;
|
|
66
64
|
} }), { stdout: uiOutput });
|
|
67
65
|
// Errors thrown while leaving this await still execute the finally block.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ViewApi } from '../domain/view-api.js';
|
|
3
|
+
import { SerializableKey, State, TreeNode } from 'dirvi-lib';
|
|
4
|
+
export type StateCodec<RuntimeState, StoredState> = {
|
|
5
|
+
schema: z.ZodType<StoredState>;
|
|
6
|
+
encode: (state: RuntimeState) => StoredState;
|
|
7
|
+
decode: (state: StoredState) => RuntimeState;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Creates a file-backed implementation of ViewApi.
|
|
11
|
+
*
|
|
12
|
+
* Each key receives its own directory, and each named view is stored as one
|
|
13
|
+
* JSON file inside that directory:
|
|
14
|
+
*
|
|
15
|
+
* <directory>/<encoded-key>/<view-name>.json
|
|
16
|
+
*
|
|
17
|
+
* The view name is validated before being used as a filename. The key is
|
|
18
|
+
* converted into one safe path segment by the caller-provided `encodeKey`
|
|
19
|
+
* function.
|
|
20
|
+
*
|
|
21
|
+
* For example:
|
|
22
|
+
*
|
|
23
|
+
* .../posix/views/<directory-hash>/project.json
|
|
24
|
+
*/
|
|
25
|
+
export declare function createFileViewApi<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, StoredState, Key>({ directory, encodeKey, stateCodec, }: {
|
|
26
|
+
directory: string;
|
|
27
|
+
encodeKey: (key: Key) => string;
|
|
28
|
+
stateCodec: StateCodec<State<Id, BufferNode>, StoredState>;
|
|
29
|
+
}): ViewApi<State<Id, BufferNode>, Key>;
|
|
30
|
+
//# sourceMappingURL=create-file-view-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-file-view-api.d.ts","sourceRoot":"","sources":["../../src/infrastructure/create-file-view-api.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,OAAO,EAAe,MAAM,uBAAuB,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE7D,MAAM,MAAM,UAAU,CAAC,YAAY,EAAE,WAAW,IAAI;IAClD,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,WAAW,CAAC;IAC7C,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,YAAY,CAAC;CAC9C,CAAC;AA2BF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,WAAW,EACX,GAAG,EACH,EACA,SAAS,EACT,SAAS,EACT,UAAU,GACX,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,CAAC;IAChC,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,EAAE,WAAW,CAAC,CAAC;CAC5D,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,EAAE,GAAG,CAAC,CA0JtC"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rename, unlink, writeFile, } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { viewNameSchema } from './view-name-schema.js';
|
|
6
|
+
function createStoredViewSchema(stateSchema) {
|
|
7
|
+
return z.object({
|
|
8
|
+
version: z.literal(1),
|
|
9
|
+
name: viewNameSchema,
|
|
10
|
+
updatedAt: z.iso.datetime(),
|
|
11
|
+
state: stateSchema,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function isNodeErrorWithCode(error, code) {
|
|
15
|
+
return error instanceof Error && 'code' in error && error.code === code;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Creates a file-backed implementation of ViewApi.
|
|
19
|
+
*
|
|
20
|
+
* Each key receives its own directory, and each named view is stored as one
|
|
21
|
+
* JSON file inside that directory:
|
|
22
|
+
*
|
|
23
|
+
* <directory>/<encoded-key>/<view-name>.json
|
|
24
|
+
*
|
|
25
|
+
* The view name is validated before being used as a filename. The key is
|
|
26
|
+
* converted into one safe path segment by the caller-provided `encodeKey`
|
|
27
|
+
* function.
|
|
28
|
+
*
|
|
29
|
+
* For example:
|
|
30
|
+
*
|
|
31
|
+
* .../posix/views/<directory-hash>/project.json
|
|
32
|
+
*/
|
|
33
|
+
export function createFileViewApi({ directory, encodeKey, stateCodec, }) {
|
|
34
|
+
const storedViewSchema = createStoredViewSchema(stateCodec.schema);
|
|
35
|
+
/**
|
|
36
|
+
* Returns the directory containing all views for one app-specific key.
|
|
37
|
+
*
|
|
38
|
+
* `encodeKey` must return one safe path segment. In particular, it must not
|
|
39
|
+
* return "/", "\\", ".", "..", or a value containing path separators.
|
|
40
|
+
*/
|
|
41
|
+
function keyDirectory(key) {
|
|
42
|
+
const encodedKey = encodeKey(key);
|
|
43
|
+
if (encodedKey.length === 0 ||
|
|
44
|
+
encodedKey === '.' ||
|
|
45
|
+
encodedKey === '..' ||
|
|
46
|
+
encodedKey.includes('/') ||
|
|
47
|
+
encodedKey.includes('\\')) {
|
|
48
|
+
throw new Error('encodeKey must return one safe path segment.');
|
|
49
|
+
}
|
|
50
|
+
return join(directory, encodedKey);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Returns the file containing one named view for a key.
|
|
54
|
+
*
|
|
55
|
+
* View names are validated by `viewNameSchema` before they are used as
|
|
56
|
+
* filenames. No encoding is necessary because invalid path characters have
|
|
57
|
+
* already been rejected.
|
|
58
|
+
*/
|
|
59
|
+
function viewFile(key, name) {
|
|
60
|
+
const validName = viewNameSchema.parse(name);
|
|
61
|
+
return join(keyDirectory(key), `${validName}.json`);
|
|
62
|
+
}
|
|
63
|
+
async function save(key, name, state) {
|
|
64
|
+
const dir = keyDirectory(key);
|
|
65
|
+
await mkdir(dir, {
|
|
66
|
+
recursive: true,
|
|
67
|
+
mode: 0o700,
|
|
68
|
+
});
|
|
69
|
+
const file = viewFile(key, name);
|
|
70
|
+
const storedState = stateCodec.encode(state);
|
|
71
|
+
const value = storedViewSchema.parse({
|
|
72
|
+
version: 1,
|
|
73
|
+
name,
|
|
74
|
+
updatedAt: new Date().toISOString(),
|
|
75
|
+
state: storedState,
|
|
76
|
+
});
|
|
77
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
78
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
mode: 0o600,
|
|
81
|
+
});
|
|
82
|
+
await rename(temporary, file);
|
|
83
|
+
}
|
|
84
|
+
async function load(key, name) {
|
|
85
|
+
try {
|
|
86
|
+
const contents = await readFile(viewFile(key, name), 'utf8');
|
|
87
|
+
const parsed = JSON.parse(contents);
|
|
88
|
+
const value = storedViewSchema.parse(parsed);
|
|
89
|
+
return stateCodec.decode(value.state);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (isNodeErrorWithCode(error, 'ENOENT')) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function list(key) {
|
|
99
|
+
const dir = keyDirectory(key);
|
|
100
|
+
let files;
|
|
101
|
+
try {
|
|
102
|
+
files = await readdir(dir);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// A key with no saved views does not have a directory yet.
|
|
106
|
+
if (isNodeErrorWithCode(error, 'ENOENT')) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
const summaries = [];
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
/*
|
|
114
|
+
* Ignore temporary files, unrelated files, and any future files that
|
|
115
|
+
* might be stored alongside view files.
|
|
116
|
+
*/
|
|
117
|
+
if (!file.endsWith('.json')) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
/*
|
|
121
|
+
* The name and timestamp are stored in the JSON envelope. Listing
|
|
122
|
+
* therefore requires reading each view file. If this becomes expensive
|
|
123
|
+
* later, metadata could be kept in a separate index.
|
|
124
|
+
*/
|
|
125
|
+
const contents = await readFile(join(dir, file), 'utf8');
|
|
126
|
+
const parsed = JSON.parse(contents);
|
|
127
|
+
const value = storedViewSchema.parse(parsed);
|
|
128
|
+
summaries.push({
|
|
129
|
+
name: value.name,
|
|
130
|
+
updatedAt: value.updatedAt,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return summaries;
|
|
134
|
+
}
|
|
135
|
+
async function remove(key, name) {
|
|
136
|
+
try {
|
|
137
|
+
// viewFile() validates the name before constructing the path.
|
|
138
|
+
await unlink(viewFile(key, name));
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
// Removing a view that does not exist is intentionally idempotent.
|
|
142
|
+
if (isNodeErrorWithCode(error, 'ENOENT')) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
save,
|
|
150
|
+
load,
|
|
151
|
+
list,
|
|
152
|
+
remove,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { UnixAbsolutePath } from '../domain/unix-path.js';
|
|
2
|
+
import { PosixTreeNode } from '../domain/posix-tree-node.js';
|
|
3
|
+
export declare function getDirEntries(address: UnixAbsolutePath): Promise<PosixTreeNode[]>;
|
|
3
4
|
//# sourceMappingURL=get-dir-entries.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-dir-entries.d.ts","sourceRoot":"","sources":["../../src/infrastructure/get-dir-entries.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"get-dir-entries.d.ts","sourceRoot":"","sources":["../../src/infrastructure/get-dir-entries.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAkB,MAAM,wBAAwB,CAAC;AAC1E,OAAO,EAAmB,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE9E,wBAAsB,aAAa,CACjC,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,aAAa,EAAE,CAAC,CAyC1B"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readlink } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { UnixPathSchema
|
|
3
|
+
import { UnixPathSchema } from '../domain/unix-path.js';
|
|
4
|
+
import { PosixNameSchema } from '../domain/posix-tree-node.js';
|
|
4
5
|
export async function getDirEntries(address) {
|
|
5
6
|
const directoryEntries = await readdir(address, {
|
|
6
7
|
withFileTypes: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-unix-abs-path.d.ts","sourceRoot":"","sources":["../../src/infrastructure/get-unix-abs-path.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"get-unix-abs-path.d.ts","sourceRoot":"","sources":["../../src/infrastructure/get-unix-abs-path.ts"],"names":[],"mappings":"AACA,OAAO,EACL,gBAAgB,EAEjB,MAAM,wBAAwB,CAAC;AAEhC,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAgBlE"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
-
import { UnixAbsolutePathSchema } from '
|
|
2
|
+
import { UnixAbsolutePathSchema, } from '../domain/unix-path.js';
|
|
3
3
|
export function getUnixAbsPath(directory) {
|
|
4
4
|
const absolutePath = resolve(directory);
|
|
5
5
|
const result = UnixAbsolutePathSchema.safeParse(absolutePath);
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import { PosixName, PosixNode } from 'dirvi-lib';
|
|
2
1
|
import { AppApi } from '../domain/app-api.js';
|
|
3
|
-
|
|
2
|
+
import { PosixName, PosixState, PosixTreeNode } from '../domain/posix-tree-node.js';
|
|
3
|
+
import { UnixAbsolutePath } from '../domain/unix-path.js';
|
|
4
|
+
export type StoredPosixFoldNode = {
|
|
5
|
+
id: PosixName;
|
|
6
|
+
children: StoredPosixFoldNode[];
|
|
7
|
+
folds: PosixName[];
|
|
8
|
+
};
|
|
9
|
+
export type StoredPosixFoldNodeRoot = {
|
|
10
|
+
children: StoredPosixFoldNode[];
|
|
11
|
+
folds: PosixName[];
|
|
12
|
+
};
|
|
13
|
+
export type StoredPosixState = Omit<PosixState, 'foldNode'> & {
|
|
14
|
+
foldNode: StoredPosixFoldNodeRoot;
|
|
15
|
+
};
|
|
16
|
+
export declare function loadPosixAppApi(directory?: string): AppApi<PosixName, PosixTreeNode, UnixAbsolutePath>;
|
|
4
17
|
//# sourceMappingURL=load-posix-app-api.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-posix-app-api.d.ts","sourceRoot":"","sources":["../../src/infrastructure/load-posix-app-api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"load-posix-app-api.d.ts","sourceRoot":"","sources":["../../src/infrastructure/load-posix-app-api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAiB,MAAM,sBAAsB,CAAC;AAM7D,OAAO,EAIL,SAAS,EAET,UAAU,EACV,aAAa,EAEd,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAkB1D,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,SAAS,CAAC;IACd,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,CAAC;AAsBF,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,GAAG;IAC5D,QAAQ,EAAE,uBAAuB,CAAC;CACnC,CAAC;AA2DF,wBAAgB,eAAe,CAC7B,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAAC,SAAS,EAAE,aAAa,EAAE,gBAAgB,CAAC,CAyBpD"}
|
|
@@ -3,18 +3,98 @@ import { watch } from 'node:fs';
|
|
|
3
3
|
import { createAppApis } from '../domain/app-api.js';
|
|
4
4
|
import { getUnixAbsPath } from './get-unix-abs-path.js';
|
|
5
5
|
import { getDirEntries } from './get-dir-entries.js';
|
|
6
|
+
import envPaths from 'env-paths';
|
|
7
|
+
import { createFileViewApi } from './create-file-view-api.js';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { PosixCursorSchema, PosixNameSchema, PosixTreeNodeSchema, } from '../domain/posix-tree-node.js';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
// ---*--- App Data ---*---
|
|
12
|
+
// Paths for app data:
|
|
13
|
+
const paths = envPaths('dirvi');
|
|
14
|
+
const posixAppDataDirectory = join(paths.data, 'apps', 'posix');
|
|
15
|
+
const posixAppViewsDirectory = join(posixAppDataDirectory, 'views');
|
|
16
|
+
// Helpers for saving app data:
|
|
17
|
+
// The key for the Posix app instance is just the directory working in.
|
|
18
|
+
function encodeKey(key) {
|
|
19
|
+
return createHash('sha256').update(key).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
const StoredPosixFoldNodeSchema = z.lazy(() => z.object({
|
|
22
|
+
id: PosixNameSchema,
|
|
23
|
+
children: z.array(StoredPosixFoldNodeSchema),
|
|
24
|
+
folds: z.array(PosixNameSchema),
|
|
25
|
+
}));
|
|
26
|
+
const StoredPosixFoldNodeRootSchema = z.object({
|
|
27
|
+
children: z.array(StoredPosixFoldNodeSchema),
|
|
28
|
+
folds: z.array(PosixNameSchema),
|
|
29
|
+
});
|
|
30
|
+
const StoredPosixStateSchema = z.object({
|
|
31
|
+
buffer: z.array(PosixTreeNodeSchema),
|
|
32
|
+
foldNode: StoredPosixFoldNodeRootSchema,
|
|
33
|
+
cursor: PosixCursorSchema,
|
|
34
|
+
});
|
|
35
|
+
// ---*--- Codec ---*---
|
|
36
|
+
function encodePosixFoldNode(node) {
|
|
37
|
+
return {
|
|
38
|
+
id: node.id,
|
|
39
|
+
children: node.children.map(encodePosixFoldNode),
|
|
40
|
+
folds: [...node.folds],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function decodePosixFoldNode(node) {
|
|
44
|
+
return {
|
|
45
|
+
id: node.id,
|
|
46
|
+
children: node.children.map(decodePosixFoldNode),
|
|
47
|
+
folds: new Set(node.folds),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function encodePosixFoldNodeRoot(root) {
|
|
51
|
+
return {
|
|
52
|
+
children: root.children.map(encodePosixFoldNode),
|
|
53
|
+
folds: [...root.folds],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function decodePosixFoldNodeRoot(root) {
|
|
57
|
+
return {
|
|
58
|
+
children: root.children.map(decodePosixFoldNode),
|
|
59
|
+
folds: new Set(root.folds),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const posixStateCodec = {
|
|
63
|
+
schema: StoredPosixStateSchema,
|
|
64
|
+
encode(state) {
|
|
65
|
+
return {
|
|
66
|
+
buffer: state.buffer,
|
|
67
|
+
foldNode: encodePosixFoldNodeRoot(state.foldNode),
|
|
68
|
+
cursor: state.cursor,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
decode(state) {
|
|
72
|
+
return {
|
|
73
|
+
buffer: state.buffer,
|
|
74
|
+
foldNode: decodePosixFoldNodeRoot(state.foldNode),
|
|
75
|
+
cursor: state.cursor,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
// --- Creating Posix App API ---
|
|
6
80
|
export function loadPosixAppApi(directory) {
|
|
7
81
|
const unixAbsPath = getUnixAbsPath(directory ?? process.cwd());
|
|
8
82
|
const apis = createAppApis();
|
|
9
|
-
const subscribeToResync = createFsResyncSubscription(join(unixAbsPath));
|
|
10
83
|
return {
|
|
84
|
+
appId: 'posix',
|
|
11
85
|
name: `Posix(${unixAbsPath})`,
|
|
12
86
|
emptyForestMessage: 'The directory is empty.',
|
|
13
87
|
loadBranches: (path) => {
|
|
14
88
|
const address = join(unixAbsPath, ...path);
|
|
15
89
|
return getDirEntries(address);
|
|
16
90
|
},
|
|
17
|
-
subscribeToResync,
|
|
91
|
+
subscribeToResync: createFsResyncSubscription(join(unixAbsPath)),
|
|
92
|
+
viewKey: unixAbsPath,
|
|
93
|
+
viewApi: createFileViewApi({
|
|
94
|
+
directory: posixAppViewsDirectory,
|
|
95
|
+
encodeKey,
|
|
96
|
+
stateCodec: posixStateCodec,
|
|
97
|
+
}),
|
|
18
98
|
...apis,
|
|
19
99
|
};
|
|
20
100
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* View names are also used as filenames.
|
|
4
|
+
*
|
|
5
|
+
* We intentionally keep the accepted format human-readable instead of
|
|
6
|
+
* encoding names into opaque filenames. The restrictions are mostly for
|
|
7
|
+
* portability between POSIX and Windows filesystems:
|
|
8
|
+
*
|
|
9
|
+
* - no path separators or control characters;
|
|
10
|
+
* - no Windows-invalid filename characters;
|
|
11
|
+
* - no leading/trailing whitespace;
|
|
12
|
+
* - no trailing periods or spaces;
|
|
13
|
+
* - no "." or "..";
|
|
14
|
+
* - no Windows device names such as "CON" or "NUL";
|
|
15
|
+
* - limited length.
|
|
16
|
+
*/
|
|
17
|
+
export declare const viewNameSchema: z.ZodString;
|
|
18
|
+
//# sourceMappingURL=view-name-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-name-schema.d.ts","sourceRoot":"","sources":["../../src/infrastructure/view-name-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2BxB;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,aA8BxB,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const windowsReservedNames = new Set([
|
|
3
|
+
'CON',
|
|
4
|
+
'PRN',
|
|
5
|
+
'AUX',
|
|
6
|
+
'NUL',
|
|
7
|
+
'COM1',
|
|
8
|
+
'COM2',
|
|
9
|
+
'COM3',
|
|
10
|
+
'COM4',
|
|
11
|
+
'COM5',
|
|
12
|
+
'COM6',
|
|
13
|
+
'COM7',
|
|
14
|
+
'COM8',
|
|
15
|
+
'COM9',
|
|
16
|
+
'LPT1',
|
|
17
|
+
'LPT2',
|
|
18
|
+
'LPT3',
|
|
19
|
+
'LPT4',
|
|
20
|
+
'LPT5',
|
|
21
|
+
'LPT6',
|
|
22
|
+
'LPT7',
|
|
23
|
+
'LPT8',
|
|
24
|
+
'LPT9',
|
|
25
|
+
]);
|
|
26
|
+
/**
|
|
27
|
+
* View names are also used as filenames.
|
|
28
|
+
*
|
|
29
|
+
* We intentionally keep the accepted format human-readable instead of
|
|
30
|
+
* encoding names into opaque filenames. The restrictions are mostly for
|
|
31
|
+
* portability between POSIX and Windows filesystems:
|
|
32
|
+
*
|
|
33
|
+
* - no path separators or control characters;
|
|
34
|
+
* - no Windows-invalid filename characters;
|
|
35
|
+
* - no leading/trailing whitespace;
|
|
36
|
+
* - no trailing periods or spaces;
|
|
37
|
+
* - no "." or "..";
|
|
38
|
+
* - no Windows device names such as "CON" or "NUL";
|
|
39
|
+
* - limited length.
|
|
40
|
+
*/
|
|
41
|
+
export const viewNameSchema = z
|
|
42
|
+
.string()
|
|
43
|
+
.min(1, 'View name cannot be empty.')
|
|
44
|
+
.max(100, 'View name cannot be longer than 100 characters.')
|
|
45
|
+
.refine((name) => name === name.trim(), {
|
|
46
|
+
message: 'View name cannot start or end with whitespace.',
|
|
47
|
+
})
|
|
48
|
+
.refine((name) => name !== '.' && name !== '..', {
|
|
49
|
+
message: 'View name cannot be "." or "..".',
|
|
50
|
+
})
|
|
51
|
+
.refine((name) => !/[\/\\]/u.test(name), {
|
|
52
|
+
message: 'View name cannot contain path separators.',
|
|
53
|
+
})
|
|
54
|
+
.refine((name) => !/[\u0000-\u001f\u007f]/u.test(name), {
|
|
55
|
+
message: 'View name cannot contain control characters.',
|
|
56
|
+
})
|
|
57
|
+
.refine((name) => !/[<>:"|?*]/u.test(name), {
|
|
58
|
+
message: 'View name contains a character that is not portable.',
|
|
59
|
+
})
|
|
60
|
+
.refine((name) => !/[. ]$/u.test(name), {
|
|
61
|
+
message: 'View name cannot end with a period or space.',
|
|
62
|
+
})
|
|
63
|
+
.refine((name) => {
|
|
64
|
+
const basename = name.split('.')[0].toUpperCase();
|
|
65
|
+
return !windowsReservedNames.has(basename);
|
|
66
|
+
}, {
|
|
67
|
+
message: 'View name is reserved by Windows.',
|
|
68
|
+
});
|
package/dist/ui/App.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { IntentToEffect, SerializableKey, State, TreeNode } from 'dirvi-lib';
|
|
|
2
2
|
import { EffectToAction } from '../application/effect-to-action.js';
|
|
3
3
|
import { AppApi } from '../domain/app-api.js';
|
|
4
4
|
import { Reducer } from '../application/reducer.js';
|
|
5
|
-
type AppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode
|
|
6
|
-
appApi: AppApi<Id, BufferNode>;
|
|
5
|
+
type AppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, ViewKey = string> = {
|
|
6
|
+
appApi: AppApi<Id, BufferNode, ViewKey>;
|
|
7
7
|
initialState: State<Id, BufferNode>;
|
|
8
8
|
reducer: Reducer<Id, BufferNode>;
|
|
9
9
|
intentToEffect: IntentToEffect<Id, BufferNode>;
|
|
@@ -12,6 +12,6 @@ type AppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, Buffer
|
|
|
12
12
|
clipboard?: (value: string) => void;
|
|
13
13
|
onError?: (error: Error) => void;
|
|
14
14
|
};
|
|
15
|
-
export declare function App<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode
|
|
15
|
+
export declare function App<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, ViewKey = string>({ appApi, initialState, reducer, intentToEffect, effectToAction, stdout, clipboard, onError, }: AppProps<Id, BufferNode, ViewKey>): import("react").JSX.Element | null;
|
|
16
16
|
export {};
|
|
17
17
|
//# sourceMappingURL=App.d.ts.map
|
package/dist/ui/App.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/ui/App.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,cAAc,EACd,eAAe,EACf,KAAK,EACL,QAAQ,EAET,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAGpE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../../src/ui/App.tsx"],"names":[],"mappings":"AAGA,OAAO,EAGL,cAAc,EACd,eAAe,EACf,KAAK,EACL,QAAQ,EAET,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAGpE,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAOpD,KAAK,QAAQ,CACX,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,OAAO,GAAG,MAAM,IACd;IACF,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IACxC,YAAY,EAAE,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IACpC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IACjC,cAAc,EAAE,cAAc,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAC/C,cAAc,EAAE,cAAc,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;IAC/C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,wBAAgB,GAAG,CACjB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,OAAO,GAAG,MAAM,EAChB,EACA,MAAM,EACN,YAAY,EACZ,OAAO,EACP,cAAc,EACd,cAAc,EACd,MAAM,EACN,SAAS,EACT,OAAO,GACR,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,sCA6NnC"}
|
package/dist/ui/App.js
CHANGED
|
@@ -6,6 +6,9 @@ import { ViewRowComponent } from './components/ViewRowComponent.js';
|
|
|
6
6
|
import { StatusBar } from './components/StatusBar.js';
|
|
7
7
|
import { inkInputToUserInput } from '../infrastructure/ink-input-to-user-input.js';
|
|
8
8
|
import { useView } from './hooks/useView.js';
|
|
9
|
+
function toError(error) {
|
|
10
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
11
|
+
}
|
|
9
12
|
export function App({ appApi, initialState, reducer, intentToEffect, effectToAction, stdout, clipboard, onError, }) {
|
|
10
13
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
11
14
|
// Give `subscribeToResync` callback a way to see current state:
|
|
@@ -94,6 +97,49 @@ export function App({ appApi, initialState, reducer, intentToEffect, effectToAct
|
|
|
94
97
|
normalBuffer: '',
|
|
95
98
|
});
|
|
96
99
|
return;
|
|
100
|
+
case 'saveView': {
|
|
101
|
+
void appApi.viewApi
|
|
102
|
+
.save(appApi.viewKey, effect.name, stateRef.current)
|
|
103
|
+
.then(() => {
|
|
104
|
+
setInputState({
|
|
105
|
+
inputMode: 'normal',
|
|
106
|
+
normalBuffer: '',
|
|
107
|
+
});
|
|
108
|
+
})
|
|
109
|
+
.catch((error) => {
|
|
110
|
+
onError?.(toError(error));
|
|
111
|
+
setExitStatus(`Unable to save view: ${toError(error).message}`);
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
case 'loadView': {
|
|
116
|
+
void appApi.viewApi
|
|
117
|
+
.load(appApi.viewKey, effect.name)
|
|
118
|
+
.then((loadedState) => {
|
|
119
|
+
if (loadedState === undefined) {
|
|
120
|
+
throw new Error(`View not found: ${effect.name}`);
|
|
121
|
+
}
|
|
122
|
+
// The saved buffer may be stale because the filesystem could have
|
|
123
|
+
// changed since the view was saved.
|
|
124
|
+
return appApi.stateApi.resync(loadedState, appApi.loadBranches);
|
|
125
|
+
})
|
|
126
|
+
.then((newState) => {
|
|
127
|
+
const oldState = stateRef.current;
|
|
128
|
+
dispatch({
|
|
129
|
+
kind: 'setState',
|
|
130
|
+
oldState,
|
|
131
|
+
newState,
|
|
132
|
+
});
|
|
133
|
+
setInputState({
|
|
134
|
+
inputMode: 'normal',
|
|
135
|
+
normalBuffer: '',
|
|
136
|
+
});
|
|
137
|
+
})
|
|
138
|
+
.catch((error) => {
|
|
139
|
+
onError?.(toError(error));
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
97
143
|
case 'quit':
|
|
98
144
|
setExitStatus(effect.exitMessage);
|
|
99
145
|
return;
|
package/dist/ui/AppShell.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export type ShellAppProps = {
|
|
|
4
4
|
clipboard?: (value: string) => void;
|
|
5
5
|
onError?: (error: Error) => void;
|
|
6
6
|
};
|
|
7
|
-
export declare function AppShell({ directory, stdout, clipboard, onError }: ShellAppProps): import("react").JSX.Element;
|
|
7
|
+
export declare function AppShell({ directory, stdout, clipboard, onError, }: ShellAppProps): import("react").JSX.Element;
|
|
8
8
|
//# sourceMappingURL=AppShell.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppShell.d.ts","sourceRoot":"","sources":["../../src/ui/AppShell.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAIF,wBAAgB,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"AppShell.d.ts","sourceRoot":"","sources":["../../src/ui/AppShell.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AAIF,wBAAgB,QAAQ,CAAC,EACvB,SAAS,EACT,MAAM,EACN,SAAS,EACT,OAAO,GACR,EAAE,aAAa,+BAYf"}
|
package/dist/ui/AppShell.js
CHANGED
|
@@ -4,7 +4,7 @@ import { LoadingApp } from './LoadingApp.js';
|
|
|
4
4
|
import { useMemo } from 'react';
|
|
5
5
|
// App Shell is for choosing the active app, then loading the api,
|
|
6
6
|
// and showing the app.
|
|
7
|
-
export function AppShell({ directory, stdout, clipboard, onError }) {
|
|
7
|
+
export function AppShell({ directory, stdout, clipboard, onError, }) {
|
|
8
8
|
// Api owns directory watcher and subscriptions
|
|
9
9
|
const posixAppApi = useMemo(() => loadPosixAppApi(directory), [directory]);
|
|
10
10
|
return (_jsx(LoadingApp, { appApi: posixAppApi, ...(stdout === undefined ? {} : { stdout }), ...(clipboard === undefined ? {} : { clipboard }), ...(onError === undefined ? {} : { onError }) }));
|
package/dist/ui/LoadingApp.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { SerializableKey, TreeNode } from 'dirvi-lib';
|
|
2
2
|
import { AppApi } from '../domain/app-api.js';
|
|
3
|
-
type LoadingAppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode
|
|
4
|
-
appApi: AppApi<Id, BufferNode>;
|
|
3
|
+
type LoadingAppProps<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, ViewKey = string> = {
|
|
4
|
+
appApi: AppApi<Id, BufferNode, ViewKey>;
|
|
5
5
|
stdout?: (message: string) => void;
|
|
6
6
|
clipboard?: (value: string) => void;
|
|
7
7
|
onError?: (error: Error) => void;
|
|
8
8
|
};
|
|
9
|
-
export declare function LoadingApp<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode
|
|
9
|
+
export declare function LoadingApp<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>, ViewKey = string>({ appApi, stdout, clipboard, onError, }: LoadingAppProps<Id, BufferNode, ViewKey>): import("react").JSX.Element;
|
|
10
10
|
export {};
|
|
11
11
|
//# sourceMappingURL=LoadingApp.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LoadingApp.d.ts","sourceRoot":"","sources":["../../src/ui/LoadingApp.tsx"],"names":[],"mappings":"AAGA,OAAO,EAEL,eAAe,EAEf,QAAQ,EACT,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAI9C,KAAK,eAAe,CAClB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"LoadingApp.d.ts","sourceRoot":"","sources":["../../src/ui/LoadingApp.tsx"],"names":[],"mappings":"AAGA,OAAO,EAEL,eAAe,EAEf,QAAQ,EACT,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAI9C,KAAK,eAAe,CAClB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,OAAO,GAAG,MAAM,IACd;IACF,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IACxC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC,CAAC;AA0BF,wBAAgB,UAAU,CACxB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAC3C,OAAO,GAAG,MAAM,EAChB,EACA,MAAM,EACN,MAAM,EACN,SAAS,EACT,OAAO,GACR,EAAE,eAAe,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,+BAgG1C"}
|
package/dist/ui/LoadingApp.js
CHANGED
|
@@ -19,7 +19,7 @@ function createInitialState(rootBranches, createEmptyFoldRoot) {
|
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
-
export function LoadingApp({ appApi, stdout, clipboard, onError }) {
|
|
22
|
+
export function LoadingApp({ appApi, stdout, clipboard, onError, }) {
|
|
23
23
|
const [initialState, setInitialState] = useState();
|
|
24
24
|
const [empty, setEmpty] = useState(false);
|
|
25
25
|
const [error, setError] = useState();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mak-98/dirvi-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "A Vim-inspired TUI for browsing directories.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
],
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"commander": "^13.0.0",
|
|
16
|
-
"dirvi-lib": "0.
|
|
16
|
+
"dirvi-lib": "0.7.0",
|
|
17
|
+
"env-paths": "^4.0.0",
|
|
17
18
|
"ink": "^7.1.1",
|
|
18
|
-
"react": "^19.0.0"
|
|
19
|
+
"react": "^19.0.0",
|
|
20
|
+
"zod": "^4.4.3"
|
|
19
21
|
},
|
|
20
22
|
"devDependencies": {
|
|
21
23
|
"@types/node": "^22.0.0",
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { SerializableKey, State, TreeNode } from 'dirvi-lib';
|
|
2
|
-
export type EventMessage<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = {
|
|
3
|
-
type: 'view';
|
|
4
|
-
view: State<Id, BufferNode>;
|
|
5
|
-
} | {
|
|
6
|
-
type: 'displayed-leaves-paths';
|
|
7
|
-
paths: Id[][];
|
|
8
|
-
} | {
|
|
9
|
-
type: 'file';
|
|
10
|
-
path: Id[];
|
|
11
|
-
};
|
|
12
|
-
//# sourceMappingURL=event-message.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"event-message.d.ts","sourceRoot":"","sources":["../../src/domain/event-message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE7D,MAAM,MAAM,YAAY,CACtB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,IAEzC;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC;CAC7B,GACD;IACE,IAAI,EAAE,wBAAwB,CAAC;IAC/B,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC;CACf,GACD;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,EAAE,EAAE,CAAC;CACZ,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"get-dir-lazy-entries.d.ts","sourceRoot":"","sources":["../../src/infrastructure/get-dir-lazy-entries.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG7D,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,SAAS,EAAE,CAAC,CAyCtB"}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { readdir, readlink } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { UnixPathSchema, PosixNameSchema } from 'dirvi-lib';
|
|
4
|
-
export async function getDirLazyEntries(address) {
|
|
5
|
-
const directoryEntries = await readdir(address, {
|
|
6
|
-
withFileTypes: true,
|
|
7
|
-
});
|
|
8
|
-
return Promise.all(directoryEntries.map(async (directoryEntry) => {
|
|
9
|
-
const name = PosixNameSchema.parse(directoryEntry.name);
|
|
10
|
-
if (directoryEntry.isSymbolicLink()) {
|
|
11
|
-
const target = UnixPathSchema.parse(await readlink(join(address, directoryEntry.name)));
|
|
12
|
-
return {
|
|
13
|
-
kind: 'symlink',
|
|
14
|
-
name,
|
|
15
|
-
target,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
if (directoryEntry.isDirectory()) {
|
|
19
|
-
return {
|
|
20
|
-
kind: 'directory',
|
|
21
|
-
name,
|
|
22
|
-
branches: null, // Directory not expanded.
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
if (directoryEntry.isFile()) {
|
|
26
|
-
return {
|
|
27
|
-
kind: 'file',
|
|
28
|
-
name,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
throw new Error(`Unsupported filesystem entry "${join(address, directoryEntry.name)}"`);
|
|
32
|
-
}));
|
|
33
|
-
}
|
|
File without changes
|