dsh-file-activity 0.5.7 → 0.5.9
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/CHANGELOG.md +50 -0
- package/README.md +29 -18
- package/lib/api-route.js +72 -76
- package/lib/bash-ops.js +65 -54
- package/lib/bash-parse.js +154 -162
- package/lib/client.js +739 -361
- package/lib/client.src.js +28 -21
- package/lib/cwd.js +6 -6
- package/lib/index.js +68 -83
- package/lib/media-route.js +119 -90
- package/lib/observer.js +30 -24
- package/lib/parts/api.part.js +1 -5
- package/lib/parts/apply.part.js +91 -82
- package/lib/parts/auto-open.part.js +83 -60
- package/lib/parts/document-previews.part.js +62 -0
- package/lib/parts/format.part.js +1 -2
- package/lib/parts/i18n.part.js +15 -1
- package/lib/parts/interceptor.part.js +16 -27
- package/lib/parts/preview-data.part.js +111 -0
- package/lib/parts/preview-float.part.js +276 -0
- package/lib/parts/rows.part.js +1 -11
- package/lib/parts/settings.part.js +71 -0
- package/lib/parts/store.part.js +19 -0
- package/lib/parts/styles.part.js +49 -3
- package/lib/parts/tree.part.js +1 -2
- package/lib/parts/view.part.js +72 -18
- package/lib/persist.js +137 -0
- package/lib/quota-trim.js +121 -0
- package/lib/quota.js +164 -0
- package/lib/state.js +336 -136
- package/lib/store.js +249 -76
- package/package.json +10 -6
- package/lib/parts/preview.part.js +0 -308
package/lib/bash-parse.js
CHANGED
|
@@ -18,34 +18,32 @@
|
|
|
18
18
|
* ('write' is classified create/modify by the known-file registry, 'delete'
|
|
19
19
|
* removes the file from stats), so the view stays consistent with the disk.
|
|
20
20
|
*/
|
|
21
|
-
import { pushCommandOps, positionalArgs, resolveSafe, pushOp } from './bash-ops.js'
|
|
22
|
-
|
|
21
|
+
import { pushCommandOps, positionalArgs, resolveSafe, pushOp } from './bash-ops.js';
|
|
23
22
|
/** Prefix wrappers that do not touch files themselves. */
|
|
24
|
-
const PREFIX_CMDS = new Set(['sudo', 'nohup', 'command', 'time', 'env'])
|
|
25
|
-
|
|
23
|
+
const PREFIX_CMDS = new Set(['sudo', 'nohup', 'command', 'time', 'env']);
|
|
26
24
|
/** Parse one bash command into [{ op: 'write'|'delete', path }] (deduped). */
|
|
27
25
|
export function parseBashFileOps(command, baseDir) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
const ops = [];
|
|
27
|
+
let cwd = baseDir;
|
|
28
|
+
for (const segment of splitSegments(command)) {
|
|
29
|
+
const tokens = tokenize(segment);
|
|
30
|
+
const start = commandStartOf(tokens);
|
|
31
|
+
if (start < 0)
|
|
32
|
+
continue;
|
|
33
|
+
const cmd = basenameOf(tokens[start].text);
|
|
34
|
+
const args = tokens.slice(start + 1);
|
|
35
|
+
if (cmd === 'cd') {
|
|
36
|
+
const target = cdTargetOf(args);
|
|
37
|
+
if (target !== '')
|
|
38
|
+
cwd = resolveSafe({ text: target, safe: true }, cwd);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
pushCommandOps(ops, cmd, args, cwd);
|
|
42
|
+
pushRedirectOps(ops, tokens, cwd);
|
|
40
43
|
}
|
|
41
|
-
|
|
42
|
-
pushRedirectOps(ops, tokens, cwd)
|
|
43
|
-
}
|
|
44
|
-
return dedupeOps(ops)
|
|
44
|
+
return dedupeOps(ops);
|
|
45
45
|
}
|
|
46
|
-
|
|
47
46
|
// ── quoting pre-pass ───────────────────────────────────────────────────────
|
|
48
|
-
|
|
49
47
|
/**
|
|
50
48
|
* Per-char quote state: 0 = unquoted, 1 = single-quoted, 2 = double-quoted,
|
|
51
49
|
* -1 = the quote character itself (kept in segment text, stripped by
|
|
@@ -54,209 +52,203 @@ export function parseBashFileOps(command, baseDir) {
|
|
|
54
52
|
* literal (bash). The output array has one entry per input character.
|
|
55
53
|
*/
|
|
56
54
|
function quoteMarks(text) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
const marks = [];
|
|
56
|
+
let quote = 0;
|
|
57
|
+
let i = 0;
|
|
58
|
+
while (i < text.length) {
|
|
59
|
+
const step = quoteStep(text, i, quote);
|
|
60
|
+
marks.push(...step.marks);
|
|
61
|
+
quote = step.quote;
|
|
62
|
+
i += step.advance;
|
|
63
|
+
}
|
|
64
|
+
return marks;
|
|
67
65
|
}
|
|
68
|
-
|
|
69
66
|
/** One character's quote transition: { marks, quote, advance }. */
|
|
70
67
|
function quoteStep(text, i, quote) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
const ch = text[i];
|
|
69
|
+
if (ch === '\\' && quote !== 1 && i + 1 < text.length)
|
|
70
|
+
return { marks: [quote, quote], quote, advance: 2 };
|
|
71
|
+
if (quote !== 0 && ch === quoteCharOf(quote))
|
|
72
|
+
return { marks: [-1], quote: 0, advance: 1 };
|
|
73
|
+
if (quote === 0 && isQuoteChar(ch))
|
|
74
|
+
return { marks: [-1], quote: quoteOf(ch), advance: 1 };
|
|
75
|
+
return { marks: [quote], quote, advance: 1 };
|
|
76
76
|
}
|
|
77
|
-
|
|
78
77
|
function quoteCharOf(quote) {
|
|
79
|
-
|
|
78
|
+
return quote === 1 ? "'" : '"';
|
|
80
79
|
}
|
|
81
|
-
|
|
82
80
|
function quoteOf(ch) {
|
|
83
|
-
|
|
81
|
+
return ch === "'" ? 1 : 2;
|
|
84
82
|
}
|
|
85
|
-
|
|
86
83
|
function isQuoteChar(ch) {
|
|
87
|
-
|
|
84
|
+
return ch === "'" || ch === '"';
|
|
88
85
|
}
|
|
89
|
-
|
|
90
86
|
// ── segment splitting ──────────────────────────────────────────────────────
|
|
91
|
-
|
|
92
87
|
/**
|
|
93
88
|
* Split a command on the top-level separators && || ; | & and newlines.
|
|
94
89
|
* Quoted separators never split; a bare `&` (background marker) splits only
|
|
95
90
|
* when followed by whitespace/end — `2>&1` stays one word.
|
|
96
91
|
*/
|
|
97
92
|
export function splitSegments(command) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
const marks = quoteMarks(command);
|
|
94
|
+
const segments = [];
|
|
95
|
+
let current = '';
|
|
96
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
97
|
+
if (marks[i] !== 0 || !isSeparator(command, i)) {
|
|
98
|
+
current += command[i];
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
flushSegment(segments, current);
|
|
102
|
+
current = '';
|
|
103
|
+
i += separatorLength(command, i) - 1;
|
|
105
104
|
}
|
|
106
|
-
flushSegment(segments, current)
|
|
107
|
-
|
|
108
|
-
i += separatorLength(command, i) - 1
|
|
109
|
-
}
|
|
110
|
-
flushSegment(segments, current)
|
|
111
|
-
return segments
|
|
105
|
+
flushSegment(segments, current);
|
|
106
|
+
return segments;
|
|
112
107
|
}
|
|
113
|
-
|
|
114
108
|
function isSeparator(command, i) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
const ch = command[i];
|
|
110
|
+
if (ch === '&')
|
|
111
|
+
return command[i + 1] === '&' || i + 1 >= command.length || /\s/.test(command[i + 1]);
|
|
112
|
+
return ch === '|' || ch === ';' || ch === '\n' || ch === '\r';
|
|
118
113
|
}
|
|
119
|
-
|
|
120
114
|
function separatorLength(command, i) {
|
|
121
|
-
|
|
115
|
+
return command[i] === '&' && command[i + 1] === '&' ? 2 : 1;
|
|
122
116
|
}
|
|
123
|
-
|
|
124
117
|
function flushSegment(segments, current) {
|
|
125
|
-
|
|
126
|
-
|
|
118
|
+
const trimmed = current.trim();
|
|
119
|
+
if (trimmed !== '')
|
|
120
|
+
segments.push(trimmed);
|
|
127
121
|
}
|
|
128
|
-
|
|
129
122
|
// ── tokenizing ─────────────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
123
|
/**
|
|
132
124
|
* Split one segment into words. Tokens keep their quoted content (quotes
|
|
133
125
|
* stripped) and a `safe` flag: false when the word contains variables /
|
|
134
126
|
* command substitution / globs — such paths must never be recorded.
|
|
135
127
|
*/
|
|
136
128
|
export function tokenize(segment) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
129
|
+
const marks = quoteMarks(segment);
|
|
130
|
+
const tokens = [];
|
|
131
|
+
let current = '';
|
|
132
|
+
let safe = true;
|
|
133
|
+
const push = () => {
|
|
134
|
+
if (current !== '') {
|
|
135
|
+
tokens.push({ text: current, safe });
|
|
136
|
+
current = '';
|
|
137
|
+
safe = true;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
for (let i = 0; i < segment.length; i += 1) {
|
|
141
|
+
const ch = segment[i];
|
|
142
|
+
if (marks[i] === -1)
|
|
143
|
+
continue; // quote characters are stripped
|
|
144
|
+
if (marks[i] === 0 && /\s/.test(ch)) {
|
|
145
|
+
push();
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (isEscape(segment, i, marks[i])) {
|
|
149
|
+
current += segment[i + 1];
|
|
150
|
+
i += 1;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (isUnsafeChar(ch, marks[i])) {
|
|
154
|
+
safe = false;
|
|
155
|
+
current += ch;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
current += ch;
|
|
159
159
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
current += ch
|
|
163
|
-
continue
|
|
164
|
-
}
|
|
165
|
-
current += ch
|
|
166
|
-
}
|
|
167
|
-
push()
|
|
168
|
-
return tokens
|
|
160
|
+
push();
|
|
161
|
+
return tokens;
|
|
169
162
|
}
|
|
170
|
-
|
|
171
163
|
/** Backslash escapes the next char outside quotes and inside double quotes. */
|
|
172
164
|
function isEscape(segment, i, quote) {
|
|
173
|
-
|
|
174
|
-
|
|
165
|
+
if (segment[i] !== '\\' || quote === 1)
|
|
166
|
+
return false;
|
|
167
|
+
return i + 1 < segment.length;
|
|
175
168
|
}
|
|
176
|
-
|
|
177
169
|
/** `$` expands everywhere except single quotes; globs only outside quotes. */
|
|
178
170
|
function isUnsafeChar(ch, quote) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
171
|
+
if (ch === '$')
|
|
172
|
+
return quote !== 1;
|
|
173
|
+
if (quote !== 0)
|
|
174
|
+
return false;
|
|
175
|
+
return ch === '`' || ch === '*' || ch === '?' || ch === '[' || ch === '{';
|
|
182
176
|
}
|
|
183
|
-
|
|
184
177
|
// ── command head ───────────────────────────────────────────────────────────
|
|
185
|
-
|
|
186
178
|
/** Index of the first real command token (skipping VAR=x and wrappers). */
|
|
187
179
|
function findStartOf(tokens, index) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
180
|
+
while (index < tokens.length) {
|
|
181
|
+
const token = tokens[index];
|
|
182
|
+
if (isAssignment(token.text) || PREFIX_CMDS.has(basenameOf(token.text))) {
|
|
183
|
+
index += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
return index;
|
|
193
187
|
}
|
|
194
|
-
return
|
|
195
|
-
}
|
|
196
|
-
return -1
|
|
188
|
+
return -1;
|
|
197
189
|
}
|
|
198
|
-
|
|
199
190
|
function commandStartOf(tokens) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
191
|
+
const start = findStartOf(tokens, 0);
|
|
192
|
+
if (start < 0)
|
|
193
|
+
return -1;
|
|
194
|
+
if (basenameOf(tokens[start].text) === 'env') {
|
|
195
|
+
// env VAR=value cmd: assignments after env are skipped too.
|
|
196
|
+
return findStartOf(tokens, start + 1);
|
|
197
|
+
}
|
|
198
|
+
return start;
|
|
207
199
|
}
|
|
208
|
-
|
|
209
200
|
function isAssignment(text) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
201
|
+
const eq = text.indexOf('=');
|
|
202
|
+
if (eq <= 0)
|
|
203
|
+
return false;
|
|
204
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(text);
|
|
213
205
|
}
|
|
214
|
-
|
|
215
206
|
function basenameOf(text) {
|
|
216
|
-
|
|
217
|
-
|
|
207
|
+
const slash = text.lastIndexOf('/');
|
|
208
|
+
return slash === -1 ? text : text.slice(slash + 1);
|
|
218
209
|
}
|
|
219
|
-
|
|
220
210
|
// ── paths & redirects ──────────────────────────────────────────────────────
|
|
221
|
-
|
|
222
211
|
/** `> file` / `>> file` / `2> file` redirects write the target file. */
|
|
223
212
|
function pushRedirectOps(ops, tokens, cwd) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
213
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
214
|
+
const match = /^(\d*)>{1,2}(.*)$/.exec(tokens[i].text);
|
|
215
|
+
if (match === null)
|
|
216
|
+
continue;
|
|
217
|
+
const target = redirectTarget(tokens, i, match[2]);
|
|
218
|
+
if (target === null)
|
|
219
|
+
continue;
|
|
220
|
+
pushOp(ops, 'write', { text: target.text, safe: target.safe }, cwd);
|
|
221
|
+
}
|
|
231
222
|
}
|
|
232
|
-
|
|
233
223
|
/** The file a redirect writes; null when it is an fd / /dev/null / missing. */
|
|
234
224
|
function redirectTarget(tokens, i, inline) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
225
|
+
if (inline !== '')
|
|
226
|
+
return usable(inline, tokens[i].safe) ? { text: inline, safe: tokens[i].safe } : null;
|
|
227
|
+
const next = tokens[i + 1];
|
|
228
|
+
if (next === undefined || !next.safe)
|
|
229
|
+
return null;
|
|
230
|
+
return usable(next.text, true) ? { text: next.text, safe: true } : null;
|
|
239
231
|
}
|
|
240
|
-
|
|
241
232
|
function usable(text, safe) {
|
|
242
|
-
|
|
243
|
-
|
|
233
|
+
if (!safe || text === '')
|
|
234
|
+
return false;
|
|
235
|
+
return !text.startsWith('&') && !text.startsWith('/dev/');
|
|
244
236
|
}
|
|
245
|
-
|
|
246
237
|
/** `cd` target: the first positional argument ('' when missing/unsafe). */
|
|
247
238
|
function cdTargetOf(args) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
239
|
+
const paths = positionalArgs(args);
|
|
240
|
+
if (paths.length === 0)
|
|
241
|
+
return '';
|
|
242
|
+
return paths[0].safe ? paths[0].text : '';
|
|
251
243
|
}
|
|
252
|
-
|
|
253
244
|
/** Dedupe by op+path, keeping the first occurrence. */
|
|
254
245
|
function dedupeOps(ops) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
246
|
+
const seen = new Set();
|
|
247
|
+
return ops.filter((op) => {
|
|
248
|
+
const key = `${op.op}\u0000${op.path}`;
|
|
249
|
+
if (seen.has(key))
|
|
250
|
+
return false;
|
|
251
|
+
seen.add(key);
|
|
252
|
+
return true;
|
|
253
|
+
});
|
|
262
254
|
}
|