dsh-file-activity 0.5.1 → 0.5.2

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.
@@ -1,149 +1,170 @@
1
- // ── row rendering helpers (recent list & stats tree) ──────────────────
2
- const opClass = (op) => (op === 'create' ? 'dfa-op-create' : op === 'modify' ? 'dfa-op-modify' : op === 'delete' ? 'dfa-op-delete' : 'dfa-op-read')
3
- const opLabel = (op) => (op === 'create' ? strings.create() : op === 'modify' ? strings.modify() : op === 'delete' ? strings.delete() : strings.read())
1
+ // ── row rendering helpers (recent list & stats tree) ──────────────────
2
+ const opClass = (op) =>
3
+ op === 'create'
4
+ ? 'dfa-op-create'
5
+ : op === 'modify'
6
+ ? 'dfa-op-modify'
7
+ : op === 'delete'
8
+ ? 'dfa-op-delete'
9
+ : 'dfa-op-read'
10
+ const opLabel = (op) =>
11
+ op === 'create'
12
+ ? strings.create()
13
+ : op === 'modify'
14
+ ? strings.modify()
15
+ : op === 'delete'
16
+ ? strings.delete()
17
+ : strings.read()
4
18
 
5
- /** Tooltip for a stats file row: absolute path + created / last-seen times. */
6
- const fileTitle = (abs, firstSeen, lastSeen) => {
7
- const times = []
8
- if (typeof firstSeen === 'number') times.push(`${strings.created()} ${formatTime(firstSeen)}`)
9
- if (typeof lastSeen === 'number') times.push(`${strings.lastSeen()} ${formatTime(lastSeen)}`)
10
- return times.length > 0 ? `${abs}\n${times.join(' · ')}` : abs
11
- }
19
+ /** Tooltip for a stats file row: absolute path + created / last-seen times. */
20
+ const fileTitle = (abs, firstSeen, lastSeen) => {
21
+ const times = []
22
+ if (typeof firstSeen === 'number') times.push(`${strings.created()} ${formatTime(firstSeen)}`)
23
+ if (typeof lastSeen === 'number') times.push(`${strings.lastSeen()} ${formatTime(lastSeen)}`)
24
+ return times.length > 0 ? `${abs}\n${times.join(' · ')}` : abs
25
+ }
12
26
 
13
- /** Count pills for a file/dir node — only actions that actually happened are
14
- * shown (a zero count renders no pill; all-zero nodes render no pill group,
15
- * keeping untouched files visually quiet). */
16
- const countPills = (node) => {
17
- const pills = []
18
- if (node.read > 0) pills.push(createElement('span', { className: 'dfa-count dfa-count-read' }, `${strings.readShort()} ${node.read}`))
19
- if (node.create > 0) pills.push(createElement('span', { className: 'dfa-count dfa-count-create' }, `${strings.createShort()} ${node.create}`))
20
- if (node.modify > 0) pills.push(createElement('span', { className: 'dfa-count dfa-count-modify' }, `${strings.modifyShort()} ${node.modify}`))
21
- if (pills.length === 0) return null
22
- return createElement('span', { className: 'dfa-counts', style: { paddingLeft: '6px' } }, ...pills)
23
- }
27
+ /** Count pills for a file/dir node — only actions that actually happened are
28
+ * shown (a zero count renders no pill; all-zero nodes render no pill group,
29
+ * keeping untouched files visually quiet). */
30
+ const countPills = (node) => {
31
+ const pills = []
32
+ if (node.read > 0)
33
+ pills.push(createElement('span', { className: 'dfa-count dfa-count-read' }, `${strings.readShort()} ${node.read}`))
34
+ if (node.create > 0)
35
+ pills.push(
36
+ createElement('span', { className: 'dfa-count dfa-count-create' }, `${strings.createShort()} ${node.create}`),
37
+ )
38
+ if (node.modify > 0)
39
+ pills.push(
40
+ createElement('span', { className: 'dfa-count dfa-count-modify' }, `${strings.modifyShort()} ${node.modify}`),
41
+ )
42
+ if (pills.length === 0) return null
43
+ return createElement('span', { className: 'dfa-counts', style: { paddingLeft: '6px' } }, ...pills)
44
+ }
24
45
 
25
- /** Extension of a file name (lowercase, no leading dot); '' when none.
26
- * Dotfiles map to their whole name ('.gitignore' → 'gitignore') so the
27
- * badge table can cover them; 'notes.' still yields ''. */
28
- const extOf = (name) => {
29
- const dot = name.lastIndexOf('.')
30
- if (dot > 0) return name.slice(dot + 1).toLowerCase()
31
- if (dot === 0) return name.slice(1).toLowerCase()
32
- return ''
33
- }
46
+ /** Extension of a file name (lowercase, no leading dot); '' when none.
47
+ * Dotfiles map to their whole name ('.gitignore' → 'gitignore') so the
48
+ * badge table can cover them; 'notes.' still yields ''. */
49
+ const extOf = (name) => {
50
+ const dot = name.lastIndexOf('.')
51
+ if (dot > 0) return name.slice(dot + 1).toLowerCase()
52
+ if (dot === 0) return name.slice(1).toLowerCase()
53
+ return ''
54
+ }
34
55
 
35
- /** Extension-less but common build files → their badge key. */
36
- const NAME_BADGES = {
37
- makefile: 'makefile', dockerfile: 'dockerfile', 'cmakelists.txt': 'cmake',
38
- }
56
+ /** Extension-less but common build files → their badge key. */
57
+ const NAME_BADGES = {
58
+ makefile: 'makefile',
59
+ dockerfile: 'dockerfile',
60
+ 'cmakelists.txt': 'cmake',
61
+ }
39
62
 
40
- /** Badge key for a file name: basename match first, then extension. */
41
- const badgeKeyOf = (name) => {
42
- const base = name.toLowerCase()
43
- const named = NAME_BADGES[base]
44
- if (named !== undefined) return named
45
- return extOf(name)
46
- }
63
+ /** Badge key for a file name: basename match first, then extension. */
64
+ const badgeKeyOf = (name) => {
65
+ const base = name.toLowerCase()
66
+ const named = NAME_BADGES[base]
67
+ if (named !== undefined) return named
68
+ return extOf(name)
69
+ }
47
70
 
48
- /** A stats-tree file row: icon + name + count pills + relative time. */
49
- const fileRow = (file, depth, onOpen) =>
50
- createElement(
51
- 'div',
52
- {
53
- key: file.abs,
54
- className: 'dfa-row',
55
- onClick: () => onOpen(file.abs),
56
- style: { paddingLeft: 8 + depth * 20 },
57
- title: fileTitle(file.abs, file.firstSeen, file.lastSeen),
58
- },
59
- createElement('span', { className: 'dfa-row-icon dfa-icon-file' }, fileIconByExt(badgeKeyOf(file.name))),
60
- createElement('span', { className: 'dfa-row-name dfa-name-file' }, file.name),
61
- countPills(file),
62
- file.lastSeen
63
- ? createElement('span', { className: 'dfa-time' }, formatRelative(file.lastSeen))
64
- : null,
65
- )
71
+ /** A stats-tree file row: icon + name + count pills + relative time. */
72
+ const fileRow = (file, depth, onOpen) =>
73
+ createElement(
74
+ 'div',
75
+ {
76
+ key: file.abs,
77
+ className: 'dfa-row',
78
+ onClick: () => onOpen(file.abs),
79
+ style: { paddingLeft: 8 + depth * 20 },
80
+ title: fileTitle(file.abs, file.firstSeen, file.lastSeen),
81
+ },
82
+ createElement('span', { className: 'dfa-row-icon dfa-icon-file' }, fileIconByExt(badgeKeyOf(file.name))),
83
+ createElement('span', { className: 'dfa-row-name dfa-name-file' }, file.name),
84
+ countPills(file),
85
+ file.lastSeen ? createElement('span', { className: 'dfa-time' }, formatRelative(file.lastSeen)) : null,
86
+ )
66
87
 
67
- /** One stats-tree node: file rows render inline, dirs toggle collapse. */
68
- function renderTreeNode(node, depth, collapsedDirs, onToggleDir, onOpen) {
69
- if (node.type === 'file') return fileRow(node, depth, onOpen)
70
- const collapsed = collapsedDirs.has(node.path)
71
- return createElement(
72
- 'div',
73
- { key: node.path },
74
- createElement(
75
- 'div',
76
- {
77
- className: 'dfa-row dfa-row-dir',
78
- onClick: () => onToggleDir(node.path),
79
- style: { paddingLeft: 8 + depth * 20 },
80
- title: `${node.path}/`,
81
- },
82
- createElement('span', { className: 'dfa-chevron' },
83
- collapsed ? icon.chevronRight(13) : icon.chevronDown(13),
84
- ),
85
- createElement('span', { className: 'dfa-row-icon dfa-icon-folder' }, icon.folder(14)),
86
- createElement('span', { className: 'dfa-row-name' },
87
- node.compressed ? node.name : node.name + '/',
88
- ),
89
- countPills(node),
90
- ),
91
- collapsed ? null : node.children.map((child) => renderTreeNode(child, depth + 1, collapsedDirs, onToggleDir, onOpen)),
92
- )
93
- }
88
+ /** One stats-tree node: file rows render inline, dirs toggle collapse. */
89
+ function renderTreeNode(node, depth, collapsedDirs, onToggleDir, onOpen) {
90
+ if (node.type === 'file') return fileRow(node, depth, onOpen)
91
+ const collapsed = collapsedDirs.has(node.path)
92
+ return createElement(
93
+ 'div',
94
+ { key: node.path },
95
+ createElement(
96
+ 'div',
97
+ {
98
+ className: 'dfa-row dfa-row-dir',
99
+ onClick: () => onToggleDir(node.path),
100
+ style: { paddingLeft: 8 + depth * 20 },
101
+ title: `${node.path}/`,
102
+ },
103
+ createElement('span', { className: 'dfa-chevron' }, collapsed ? icon.chevronRight(13) : icon.chevronDown(13)),
104
+ createElement('span', { className: 'dfa-row-icon dfa-icon-folder' }, icon.folder(14)),
105
+ createElement('span', { className: 'dfa-row-name' }, node.compressed ? node.name : node.name + '/'),
106
+ countPills(node),
107
+ ),
108
+ collapsed
109
+ ? null
110
+ : node.children.map((child) => renderTreeNode(child, depth + 1, collapsedDirs, onToggleDir, onOpen)),
111
+ )
112
+ }
94
113
 
95
- /** A recent-list row: op badge + basename + relative time. */
96
- const recentEntry = (entry, onOpen) =>
97
- createElement(
98
- 'div',
99
- {
100
- key: `${entry.path}:${entry.time}:${entry.op}`,
101
- className: 'dfa-row',
102
- onClick: () => onOpen(entry.path),
103
- title: entry.path,
104
- },
105
- createElement('span', { className: `dfa-op ${opClass(entry.op)}` }, opLabel(entry.op)),
106
- createElement('span', { className: 'dfa-row-name' }, basenameOf(entry.path)),
107
- createElement('span', { className: 'dfa-time' }, formatRelative(entry.time)),
108
- )
114
+ /** A recent-list row: op badge + basename + relative time. */
115
+ const recentEntry = (entry, onOpen) =>
116
+ createElement(
117
+ 'div',
118
+ {
119
+ key: `${entry.path}:${entry.time}:${entry.op}`,
120
+ className: 'dfa-row',
121
+ onClick: () => onOpen(entry.path),
122
+ title: entry.path,
123
+ },
124
+ createElement('span', { className: `dfa-op ${opClass(entry.op)}` }, opLabel(entry.op)),
125
+ createElement('span', { className: 'dfa-row-name' }, basenameOf(entry.path)),
126
+ createElement('span', { className: 'dfa-time' }, formatRelative(entry.time)),
127
+ )
128
+
129
+ /** Toggle a key in a Set (directory collapse state). */
130
+ function toggleInSet(set, key) {
131
+ const next = new Set(set)
132
+ if (next.has(key)) next.delete(key)
133
+ else next.add(key)
134
+ return next
135
+ }
109
136
 
110
- /** Toggle a key in a Set (directory collapse state). */
111
- function toggleInSet(set, key) {
112
- const next = new Set(set)
113
- if (next.has(key)) next.delete(key)
114
- else next.add(key)
115
- return next
116
- }
137
+ /** Clear the current session's records host-side and reset its bucket. */
138
+ function clearSessionData(dataStore, sessionId) {
139
+ if (!window.confirm(strings.clearConfirm())) return
140
+ postClear(sessionId)
141
+ const current = dataStore.getSnapshot()
142
+ dataStore.set({
143
+ bySession: {
144
+ ...(current.bySession ?? {}),
145
+ [sessionId]: { recent: [], counts: {}, loading: false },
146
+ },
147
+ })
148
+ }
117
149
 
118
- /** Clear the current session's records host-side and reset its bucket. */
119
- function clearSessionData(dataStore, sessionId) {
120
- if (!window.confirm(strings.clearConfirm())) return
121
- postClear(sessionId)
150
+ /** Manual refresh: fetch stats + the authoritative cwd for this session. */
151
+ function refreshSessionData(dataStore, sessionId, setCwd, setError) {
152
+ if (sessionId === '') return
153
+ void fetchStats(sessionId)
154
+ .then((value) => {
155
+ if (value === null) return
156
+ setCwd((prev) => prev || value.cwd || '')
122
157
  const current = dataStore.getSnapshot()
123
158
  dataStore.set({
124
159
  bySession: {
125
160
  ...(current.bySession ?? {}),
126
- [sessionId]: { recent: [], counts: {}, loading: false },
161
+ [sessionId]: { recent: value.recent ?? [], counts: value.counts ?? {}, loading: false },
127
162
  },
128
163
  })
129
- }
130
-
131
- /** Manual refresh: fetch stats + the authoritative cwd for this session. */
132
- function refreshSessionData(dataStore, sessionId, setCwd, setError) {
133
- if (sessionId === '') return
134
- void fetchStats(sessionId).then((value) => {
135
- if (value === null) return
136
- setCwd((prev) => prev || value.cwd || '')
137
- const current = dataStore.getSnapshot()
138
- dataStore.set({
139
- bySession: {
140
- ...(current.bySession ?? {}),
141
- [sessionId]: { recent: value.recent ?? [], counts: value.counts ?? {}, loading: false },
142
- },
143
- })
144
- setError(false)
145
- }).catch(() => setError(true))
146
- void fetchSessionCwd(sessionId).then((cwd) => {
147
- if (cwd !== '') setCwd(cwd)
148
- })
149
- }
164
+ setError(false)
165
+ })
166
+ .catch(() => setError(true))
167
+ void fetchSessionCwd(sessionId).then((cwd) => {
168
+ if (cwd !== '') setCwd(cwd)
169
+ })
170
+ }
@@ -1,16 +1,16 @@
1
- // ── tiny external store ───────────────────────────────────────────────
2
- function createStore(initial) {
3
- let state = initial
4
- const listeners = new Set()
5
- return {
6
- getSnapshot: () => state,
7
- set(patch) {
8
- state = { ...state, ...patch }
9
- for (const listener of [...listeners]) listener()
10
- },
11
- subscribe(listener) {
12
- listeners.add(listener)
13
- return () => listeners.delete(listener)
14
- },
15
- }
16
- }
1
+ // ── tiny external store ───────────────────────────────────────────────
2
+ function createStore(initial) {
3
+ let state = initial
4
+ const listeners = new Set()
5
+ return {
6
+ getSnapshot: () => state,
7
+ set(patch) {
8
+ state = { ...state, ...patch }
9
+ for (const listener of [...listeners]) listener()
10
+ },
11
+ subscribe(listener) {
12
+ listeners.add(listener)
13
+ return () => listeners.delete(listener)
14
+ },
15
+ }
16
+ }
@@ -1,8 +1,8 @@
1
- // ── themed stylesheet (injected once per activation) ──────────────────
2
- // Mirrors the better-sidebar explorer surface: tight 2px 6px 8px body,
3
- // 30px rows, box-sizing border-box indentation, folder rows use the
4
- // strong type face to read as directories, files stay regular.
5
- const STYLES = `
1
+ // ── themed stylesheet (injected once per activation) ──────────────────
2
+ // Mirrors the better-sidebar explorer surface: tight 2px 6px 8px body,
3
+ // 30px rows, box-sizing border-box indentation, folder rows use the
4
+ // strong type face to read as directories, files stay regular.
5
+ const STYLES = `
6
6
  .dfa { display:flex; flex-direction:column; height:100%; overflow-y:auto; overflow-x:hidden;
7
7
  padding:2px 6px 8px; gap:2px; font:var(--dsw-font-s-14); color:var(--dsw-alias-label-primary); }
8
8
  .dfa-iconbtn { display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px; padding:0;
@@ -74,20 +74,7 @@
74
74
  .dfa-pdf-download:hover { text-decoration:underline; }
75
75
  .dfa-pdf-frame { flex:1; min-height:0; width:100%; border:none; border-radius:6px; background:transparent; }
76
76
  @keyframes dfa-row-in { from { opacity:0; transform:translateY(1px); } to { opacity:1; transform:none; } }
77
- /* ── sidebar tab selected-state overrides (issue #25) ─────────────────────
78
- The host (dsh-better-sidebar) renders tabs with CSS-modules hashed class
79
- names (e.g. tabActive_xxxxx) and no data attributes, so the selected tab
80
- is targeted via the [class*="tabActive"] substring selector — the fallback
81
- contract that keeps these rules working if the host ever renames its
82
- classes. The double attribute selector [class*="tab"][class*="tabActive"]
83
- (0,2,0) beats the host .tabActive (0,1,0) by specificity alone: no
84
- !important, no reliance on injection order. Selected = theme-aware brand
85
- fill + contrast ink (light #4176e6/#fff, dark #679efe/#0f1115); the :hover
86
- variant (0,3,0) beats the host .tab:hover (0,2,0) so the selected state
87
- never collapses into the host's grey hover. Unselected tabs stay untouched
88
- (transparent + secondary label ink, host grey hover). */
89
- [class*="tab"][class*="tabActive"] { background:var(--dsw-alias-state-business-primary); color:var(--dsw-alias-label-primary-foreground); }
90
- [class*="tab"][class*="tabActive"]:hover { background:var(--dsw-alias-state-business-primary); color:var(--dsw-alias-label-primary-foreground); }
91
- [class*="tab"][class*="tabActive"] [class*="tabClose"] { color:var(--dsw-alias-label-primary-foreground); }
92
- [class*="tab"][class*="tabActive"] [class*="tabClose"]:hover { background:color-mix(in srgb, var(--dsw-alias-label-primary-foreground) 18%, transparent); color:var(--dsw-alias-label-primary-foreground); }
77
+ /* issue #60: 移除 #25 的侧边栏页签选中态品牌蓝覆盖([class*="tab"][class*=
78
+ "tabActive"] 全局子串选择器误伤宿主对话/工作区 tab 选中态,出现用户不
79
+ 想要的蓝色高亮)。页签选中态回归宿主(dsh-better-sidebar)默认样式。 */
93
80
  `
@@ -1,71 +1,84 @@
1
- // ── directory tree construction ───────────────────────────────────────
2
- /**
3
- * Collapse chain directories: a directory whose only child is another
4
- * directory merges into it (a → a.b → a.b.c …). Deep single-child paths
5
- * render as one dotted label with the file(s) directly beneath.
6
- * `root` itself is never collapsed (its name is '' and would drop the
7
- * top-level directory).
8
- */
9
- function compressChains(node, isRoot) {
10
- for (const child of node.children) {
11
- if (child.type === 'dir') compressChains(child, false)
12
- }
13
- if (isRoot) return
14
- while (node.children.length === 1 && node.children[0].type === 'dir') {
15
- const only = node.children[0]
16
- node.name = `${node.name}.${only.name}`
17
- node.children = only.children
18
- node.compressed = true
19
- }
20
- }
1
+ // ── directory tree construction ───────────────────────────────────────
2
+ /**
3
+ * Collapse chain directories: a directory whose only child is another
4
+ * directory merges into it (a → a.b → a.b.c …). Deep single-child paths
5
+ * render as one dotted label with the file(s) directly beneath.
6
+ * `root` itself is never collapsed (its name is '' and would drop the
7
+ * top-level directory).
8
+ */
9
+ function compressChains(node, isRoot) {
10
+ for (const child of node.children) {
11
+ if (child.type === 'dir') compressChains(child, false)
12
+ }
13
+ if (isRoot) return
14
+ while (node.children.length === 1 && node.children[0].type === 'dir') {
15
+ const only = node.children[0]
16
+ node.name = `${node.name}.${only.name}`
17
+ node.children = only.children
18
+ node.compressed = true
19
+ }
20
+ }
21
21
 
22
- /**
23
- * Sort a directory node: directories first (alphabetically), then files
24
- * (by total activity, then name); recurse into directories.
25
- */
26
- function sortNode(node) {
27
- node.children.sort((a, b) => {
28
- if (a.type !== b.type) return a.type === 'dir' ? -1 : 1
29
- if (a.type === 'dir') return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
30
- const ta = a.read + a.create + a.modify
31
- const tb = b.read + b.create + b.modify
32
- return tb - ta || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
33
- })
34
- for (const child of node.children) {
35
- if (child.type === 'dir') sortNode(child)
36
- }
37
- }
22
+ /**
23
+ * Sort a directory node: directories first (alphabetically), then files
24
+ * (by total activity, then name); recurse into directories.
25
+ */
26
+ function sortNode(node) {
27
+ node.children.sort((a, b) => {
28
+ if (a.type !== b.type) return a.type === 'dir' ? -1 : 1
29
+ if (a.type === 'dir') return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
30
+ const ta = a.read + a.create + a.modify
31
+ const tb = b.read + b.create + b.modify
32
+ return tb - ta || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
33
+ })
34
+ for (const child of node.children) {
35
+ if (child.type === 'dir') sortNode(child)
36
+ }
37
+ }
38
38
 
39
- /**
40
- * Build a nested directory tree from per-file counts, keyed by the file's
41
- * absolute path. Every directory node aggregates its subtree counters and
42
- * sorts directories first (alphabetically), then files (by activity).
43
- */
44
- function buildTree(counts) {
45
- const root = { type: 'dir', name: '', path: '', children: [], read: 0, create: 0, modify: 0 }
46
- for (const [abs, counter] of Object.entries(counts)) {
47
- const parts = abs.split('/').filter((part) => part !== '')
48
- if (parts.length === 0) continue
49
- const name = parts[parts.length - 1]
50
- let node = root
51
- for (const dir of parts.slice(0, -1)) {
52
- let child = node.children.find((c) => c.type === 'dir' && c.name === dir)
53
- if (child === undefined) {
54
- child = { type: 'dir', name: dir, path: `${node.path}/${dir}`, children: [], read: 0, create: 0, modify: 0 }
55
- node.children.push(child)
56
- }
57
- node = child
58
- node.read += counter.read
59
- node.create += counter.create
60
- node.modify += counter.modify
39
+ /**
40
+ * Build a nested directory tree from per-file counts, keyed by the file's
41
+ * absolute path. Every directory node aggregates its subtree counters and
42
+ * sorts directories first (alphabetically), then files (by activity).
43
+ */
44
+ function buildTree(counts) {
45
+ const root = { type: 'dir', name: '', path: '', children: [], read: 0, create: 0, modify: 0 }
46
+ for (const [abs, counter] of Object.entries(counts)) {
47
+ const parts = abs.split('/').filter((part) => part !== '')
48
+ if (parts.length === 0) continue
49
+ const name = parts[parts.length - 1]
50
+ let node = root
51
+ for (const dir of parts.slice(0, -1)) {
52
+ let child = node.children.find((c) => c.type === 'dir' && c.name === dir)
53
+ if (child === undefined) {
54
+ child = {
55
+ type: 'dir',
56
+ name: dir,
57
+ path: `${node.path}/${dir}`,
58
+ children: [],
59
+ read: 0,
60
+ create: 0,
61
+ modify: 0,
61
62
  }
62
- node.children.push({
63
- type: 'file', name, abs,
64
- read: counter.read, create: counter.create, modify: counter.modify,
65
- firstSeen: counter.firstSeen, lastSeen: counter.lastSeen,
66
- })
63
+ node.children.push(child)
67
64
  }
68
- sortNode(root)
69
- compressChains(root, true)
70
- return root
65
+ node = child
66
+ node.read += counter.read
67
+ node.create += counter.create
68
+ node.modify += counter.modify
71
69
  }
70
+ node.children.push({
71
+ type: 'file',
72
+ name,
73
+ abs,
74
+ read: counter.read,
75
+ create: counter.create,
76
+ modify: counter.modify,
77
+ firstSeen: counter.firstSeen,
78
+ lastSeen: counter.lastSeen,
79
+ })
80
+ }
81
+ sortNode(root)
82
+ compressChains(root, true)
83
+ return root
84
+ }