@needmoretruth/nmts-cli 0.36.3 → 0.38.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/CHANGELOG.md +17 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/dist/artifact-about.d.ts +1 -1
- package/dist/commands/erase.js +1 -1
- package/dist/commands/organise.d.ts +5 -27
- package/dist/commands/organise.js +33 -193
- package/dist/commands/push.js +1 -1
- package/dist/commands/s3.d.ts +0 -10
- package/dist/commands/s3.js +46 -121
- package/dist/commands/trash.d.ts +0 -9
- package/dist/commands/trash.js +23 -223
- package/dist/drive-edit.d.ts +189 -0
- package/dist/drive-edit.js +541 -0
- package/dist/product.d.ts +1 -1
- package/dist/product.js +1 -1
- package/dist/s3/drive.d.ts +108 -0
- package/dist/s3/drive.js +136 -0
- package/dist/s3/listing.d.ts +8 -1
- package/dist/s3/listing.js +8 -1
- package/dist/s3/server.d.ts +42 -3
- package/dist/s3/server.js +59 -20
- package/dist/s3/sigv4.d.ts +26 -0
- package/dist/s3/sigv4.js +37 -0
- package/dist/s3/xml.d.ts +8 -1
- package/dist/s3/xml.js +13 -4
- package/dist/s3-gateway.d.ts +8 -0
- package/dist/s3-gateway.js +13 -0
- package/docs/commands/create.md +2 -2
- package/docs/commands/env.md +1 -1
- package/docs/commands/extend.md +0 -3
- package/docs/commands/login.md +1 -1
- package/docs/commands/logout.md +1 -1
- package/docs/commands/marks.md +1 -1
- package/docs/commands/mcp.md +1 -1
- package/docs/commands/put.md +2 -2
- package/docs/commands/wallet.md +7 -9
- package/docs/commands/whoami.md +2 -2
- package/package.json +9 -1
package/dist/commands/trash.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ManifestEntry } from "../shared/lib/drive/manifest-codec.ts";
|
|
2
1
|
export interface TrashOptions {
|
|
3
2
|
server?: string | undefined;
|
|
4
3
|
network?: string | undefined;
|
|
@@ -7,11 +6,3 @@ export interface TrashOptions {
|
|
|
7
6
|
}
|
|
8
7
|
export declare function rm(paths: readonly string[], options?: TrashOptions): Promise<number>;
|
|
9
8
|
export declare function restore(paths: readonly string[], options?: TrashOptions): Promise<number>;
|
|
10
|
-
/**
|
|
11
|
-
* Every file at or under one entry.
|
|
12
|
-
*
|
|
13
|
-
* ⚠ Trashed descendants are INCLUDED HERE, and the CALLER filters. Somebody who trashed one file
|
|
14
|
-
* last week and then trashes its folder expects the folder to be gone from the server too — so
|
|
15
|
-
* `rm` takes this set whole. `restore` cannot: see the note at the call site.
|
|
16
|
-
*/
|
|
17
|
-
export declare function filesUnder(entries: readonly ManifestEntry[], rootId: string): ManifestEntry[];
|
package/dist/commands/trash.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// `nmts rm` and `nmts restore` — the two halves of the trash.
|
|
2
2
|
//
|
|
3
|
+
// ⛔ WHAT THEY DO IS IN `drive-edit.ts`; WHAT IS HERE IS THE TERMINAL. The server rows, the list
|
|
4
|
+
// write and every refusal about the list moved out the day the SDK needed the same two verbs
|
|
5
|
+
// without one — and the order of the two (row first, list second) is the rule a second copy
|
|
6
|
+
// would be most likely to get backwards.
|
|
7
|
+
//
|
|
3
8
|
// ⛔ NEITHER OF THESE DESTROYS ANYTHING. `rm` moves everything it is given to the trash, where it
|
|
4
9
|
// stays restorable for thirty days; the endpoint that erases a stored row for good is closed to
|
|
5
10
|
// an API key and stays closed, so no command here can reach it.
|
|
@@ -18,25 +23,9 @@
|
|
|
18
23
|
// said no verb without an undo would ever live in this tool; that stopped being true the day
|
|
19
24
|
// the sweep arrived, and a header that goes on describing the world before a change is the way
|
|
20
25
|
// the next reader is misled.
|
|
21
|
-
|
|
22
|
-
// ⛔ THE SERVER ROW GOES FIRST, AND "ALREADY DONE" COUNTS AS DONE. The order is not arbitrary and
|
|
23
|
-
// neither is the forgiveness:
|
|
24
|
-
// · A trashed item's bytes cannot be fetched — `GET /v1/items/{id}/parts` requires
|
|
25
|
-
// `deleted_at IS NULL`. So the state to avoid above all others is a drive that shows a file
|
|
26
|
-
// as live when the server has already trashed it: the person sees it, asks for it, and is
|
|
27
|
-
// told it does not exist.
|
|
28
|
-
// · Writing the list only after the server agreed means a failed server call leaves the
|
|
29
|
-
// drive exactly as it was, which is the state a person can act on.
|
|
30
|
-
// · And a 404 from the server means the row is already in the state being asked for, which
|
|
31
|
-
// is what an interrupted run leaves behind. Treating it as a failure would make the retry
|
|
32
|
-
// of a half-finished command impossible — the one moment the retry is needed.
|
|
33
|
-
import { setTrashed } from "../item-trash.js";
|
|
34
|
-
import { buildIndex, fullPathOf, isLive, KIND_FILE } from "../drive-paths.js";
|
|
26
|
+
import { trashPaths } from "../drive-edit.js";
|
|
35
27
|
import { NmtsError } from "../errors.js";
|
|
36
|
-
import { readFileList } from "../manifest.js";
|
|
37
|
-
import { applyManyToList, batchTargets } from "../manifest-write.js";
|
|
38
28
|
import { openSession } from "../session.js";
|
|
39
|
-
import { applyIntent } from "../shared/lib/drive/manifest-ops.js";
|
|
40
29
|
export async function rm(paths, options = {}) {
|
|
41
30
|
return run("rm", paths, options);
|
|
42
31
|
}
|
|
@@ -52,150 +41,30 @@ async function run(verb, paths, options) {
|
|
|
52
41
|
});
|
|
53
42
|
}
|
|
54
43
|
const session = await openSession(options);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// A reason that does not hold teaches the next reader to keep the wrong branch.)
|
|
62
|
-
//
|
|
63
|
-
// ⛔ AND ONE PATH THAT WILL NOT RESOLVE REFUSES THE WHOLE RUN, before a single server row is
|
|
64
|
-
// touched. Trashing four of the five things somebody named and exiting 0 is worse than
|
|
65
|
-
// trashing none: the run reads as done, and finding the odd one out means diffing the drive.
|
|
66
|
-
const index = buildIndex(entries);
|
|
67
|
-
const found = batchTargets(entries, paths, {
|
|
68
|
-
...(verb === "restore" ? { includeTrashed: true } : {}),
|
|
69
|
-
nothingHappened: "Nothing changed.",
|
|
70
|
-
});
|
|
71
|
-
const acting = [];
|
|
72
|
-
const skipped = [];
|
|
73
|
-
for (const entry of found) {
|
|
74
|
-
const at = fullPathOf(index, entry);
|
|
75
|
-
if (verb === "restore" && isLive(index, entry)) {
|
|
76
|
-
// Already in the state being asked for. Not a refusal — named, and left alone.
|
|
77
|
-
skipped.push(at);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (verb === "restore" && entry.deletedAt === undefined) {
|
|
81
|
-
// In the trash, but only because something above it is. Restoring this row would clear a
|
|
82
|
-
// `deletedAt` it does not have and leave the person exactly where they were.
|
|
83
|
-
throw new NmtsError(`"${at}" is in the trash because a folder above it is.`, {
|
|
84
|
-
exitCode: 4,
|
|
85
|
-
nextStep: `Nothing changed. Restore that folder instead — \`nmts ls --all\` shows which one carries the trash.`,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
acting.push({ entry, path: at });
|
|
89
|
-
}
|
|
90
|
-
// ⛔ NAMING A FOLDER AND SOMETHING INSIDE IT IS NAMING ONE TRASHING TWICE, and only for `rm` is
|
|
91
|
-
// that a problem worth solving here: stamping the child as well would give it a thirty-day
|
|
92
|
-
// clock of its own, and then restoring the folder would leave it behind — the person would
|
|
93
|
-
// have to remember they had also named it to ever find it again. Its bytes are covered
|
|
94
|
-
// either way, because the rows are read from the folder. `restore` is the opposite case: a
|
|
95
|
-
// child with its own instant needs its own clearing, so nothing is dropped there.
|
|
96
|
-
const named = new Set(acting.map((t) => t.entry.id));
|
|
97
|
-
const covered = verb === "rm" ? acting.filter((t) => hasNamedAncestor(entries, t.entry, named)) : [];
|
|
98
|
-
const targets = acting.filter((t) => !covered.includes(t));
|
|
99
|
-
for (const t of covered)
|
|
100
|
-
skipped.push(t.path);
|
|
101
|
-
if (targets.length === 0) {
|
|
102
|
-
// Everything named was already where it was asked to be. A no-op is a success: writing the
|
|
103
|
-
// list would cost every other device a download for nothing.
|
|
104
|
-
return nothingToDo(say, options, verb, skipped, list.seq ?? 0);
|
|
105
|
-
}
|
|
106
|
-
// Every FILE at or under the targets — a folder holds no bytes and has no server row, so the
|
|
107
|
-
// rows to move are its file descendants.
|
|
108
|
-
//
|
|
109
|
-
// ⛔ THE ROWS TO MOVE ARE THE ONES THE EDIT WILL MAKE REACHABLE, so the set is read off a
|
|
110
|
-
// PREVIEW of the list rather than guessed (2026-08-23). `rm` is easy — everything
|
|
111
|
-
// under the target loses its bytes. `restore` is not: a file the person deleted separately
|
|
112
|
-
// last week keeps its own `deletedAt`, stays in the trash after the folder comes back, and
|
|
113
|
-
// its row must stay deleted with it. Restoring that row would cancel its own thirty-day
|
|
114
|
-
// sweep, go on costing storage, and leave the list saying "trashed" while the server says
|
|
115
|
-
// "live" — after which `rm` refuses to put it back and the tool has no way out.
|
|
116
|
-
const at = Date.now();
|
|
117
|
-
const ids = targets.map((t) => t.entry.id);
|
|
118
|
-
const preview = buildIndex(applyIntent(entries, intentFor(verb, ids, at)));
|
|
119
|
-
const under = uniqueById(targets.flatMap((t) => filesUnder(entries, t.entry.id)));
|
|
120
|
-
// ⚠ Judged on the PREVIEW's own row, not on the one in hand: `applyIntent` returns new objects,
|
|
121
|
-
// so asking the preview about the old object reads the old `deletedAt` and answers "still
|
|
122
|
-
// trashed" for the very thing being restored.
|
|
123
|
-
const files = verb === "rm"
|
|
124
|
-
? under
|
|
125
|
-
: under.filter((f) => {
|
|
126
|
-
const after = preview.byId.get(f.id);
|
|
127
|
-
return after !== undefined && isLive(preview, after);
|
|
128
|
-
});
|
|
129
|
-
let done = 0;
|
|
130
|
-
try {
|
|
131
|
-
for (const file of files) {
|
|
132
|
-
await tellServer(session.server, session.apiKey, verb, file.id);
|
|
133
|
-
done += 1;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
catch (error) {
|
|
137
|
-
// ⛔ A HALF-FINISHED RUN MUST NAME ITSELF. Without this the agent sees six words of stderr and
|
|
138
|
-
// the tool's own guidance ("a refusal is not a transient error, do not retry in a loop")
|
|
139
|
-
// steers it away from the one thing that fixes this — running the same command again.
|
|
140
|
-
const because = error instanceof Error ? error.message : "the server refused";
|
|
141
|
-
throw new NmtsError(because, {
|
|
142
|
-
exitCode: 1,
|
|
143
|
-
nextStep: `${done} of ${files.length} file rows were moved before this stopped, and the file list was ` +
|
|
144
|
-
`not written. Running \`nmts ${verb}\` on the same paths again finishes the job — nothing is lost.`,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
// ⛔ THE IDS ARE DECIDED AGAIN ON EVERY ATTEMPT, and this is not ceremony. Between the read
|
|
148
|
-
// above and the write below another device can put one of these targets in the trash — by
|
|
149
|
-
// trashing it, or by moving it under a folder that already is. Re-applying the intent we
|
|
150
|
-
// built earlier would then stamp `deletedAt` on something that is ALREADY in the trash by
|
|
151
|
-
// inheritance, giving it a clock of its own and quietly detaching it from the folder it came
|
|
152
|
-
// with: restoring that folder afterwards would leave it behind. An id that has left the list
|
|
153
|
-
// entirely is dropped for the reason `manifest-ops.ts` gives — the other device removing it
|
|
154
|
-
// is newer information than our edit, and putting it back would undo a deletion somebody
|
|
155
|
-
// made on purpose.
|
|
156
|
-
const result = await applyManyToList(session, (now) => {
|
|
157
|
-
const nowIndex = buildIndex(now);
|
|
158
|
-
const still = ids.filter((id) => {
|
|
159
|
-
const live = nowIndex.byId.get(id);
|
|
160
|
-
if (live === undefined)
|
|
161
|
-
return false;
|
|
162
|
-
return verb === "rm" ? isLive(nowIndex, live) : live.deletedAt !== undefined;
|
|
163
|
-
});
|
|
164
|
-
return still.length === 0 ? [] : [intentFor(verb, still, at)];
|
|
165
|
-
});
|
|
166
|
-
const shown = targets.map((t) => t.path);
|
|
44
|
+
// ⛔ WITHOUT `strict`. A person gets a line saying what was already out of the trash and keeps
|
|
45
|
+
// the rest of their run; a program gets a refusal, because it cannot read the line. The
|
|
46
|
+
// difference is spelled once, where the option is declared.
|
|
47
|
+
const outcome = await trashPaths(session, verb, paths);
|
|
48
|
+
// ⚠ The outcome's own field order IS this output. Nothing is assembled here, so the two can
|
|
49
|
+
// never drift into saying different things about the same run.
|
|
167
50
|
if (options.json) {
|
|
168
|
-
say(JSON.stringify(
|
|
169
|
-
paths: shown,
|
|
170
|
-
ids,
|
|
171
|
-
files: files.length,
|
|
172
|
-
// Named, and nothing was written for them: already out of the trash, or already covered
|
|
173
|
-
// by a folder in the same run.
|
|
174
|
-
skipped,
|
|
175
|
-
changed: result.changed,
|
|
176
|
-
// ⚠ The one signal that another writer intervened. It was printed in prose and left out of
|
|
177
|
-
// the JSON, which is the half an agent is told to read (2026-08-23).
|
|
178
|
-
reappliedAfterConflict: result.reappliedAfterConflict,
|
|
179
|
-
seq: result.seq,
|
|
180
|
-
}));
|
|
51
|
+
say(JSON.stringify(outcome));
|
|
181
52
|
return 0;
|
|
182
53
|
}
|
|
183
|
-
|
|
184
|
-
|
|
54
|
+
if (outcome.paths.length === 0)
|
|
55
|
+
return nothingToDo(say, verb, outcome.skipped);
|
|
56
|
+
const moved = outcome.files === 1 ? "1 file" : `${outcome.files} files`;
|
|
57
|
+
const names = outcome.paths.map((s) => `"${s}"`).join(", ");
|
|
185
58
|
say(verb === "rm"
|
|
186
|
-
? `Moved ${names} to the trash (${moved}). ${
|
|
59
|
+
? `Moved ${names} to the trash (${moved}). ${outcome.paths.length === 1 ? "It" : "They"} can be restored for 30 days.`
|
|
187
60
|
: `Restored ${names} (${moved}).`);
|
|
188
|
-
if (skipped.length > 0)
|
|
189
|
-
say(` ${skippedLine(verb, skipped)}`);
|
|
190
|
-
if (
|
|
61
|
+
if (outcome.skipped.length > 0)
|
|
62
|
+
say(` ${skippedLine(verb, outcome.skipped)}`);
|
|
63
|
+
if (outcome.reappliedAfterConflict) {
|
|
191
64
|
say(` Another device wrote the file list first, so this was applied to that version.`);
|
|
192
65
|
}
|
|
193
66
|
return 0;
|
|
194
67
|
}
|
|
195
|
-
/** The one intent either verb writes. Built in two places, so it is spelled in one. */
|
|
196
|
-
function intentFor(verb, ids, at) {
|
|
197
|
-
return verb === "rm" ? { op: "trash", ids, at } : { op: "restore", ids, at };
|
|
198
|
-
}
|
|
199
68
|
/** What was named but not acted on, in the words that say why. */
|
|
200
69
|
function skippedLine(verb, skipped) {
|
|
201
70
|
const names = skipped.map((s) => `"${s}"`).join(", ");
|
|
@@ -204,85 +73,16 @@ function skippedLine(verb, skipped) {
|
|
|
204
73
|
: `${names} was not in the trash.`;
|
|
205
74
|
}
|
|
206
75
|
/**
|
|
207
|
-
* The run had nothing to do.
|
|
76
|
+
* The run had nothing to do.
|
|
208
77
|
*
|
|
209
78
|
* ⚠ In practice this is the `restore` case — everything named was already out of the trash.
|
|
210
79
|
* `rm` reaches it only if every path it resolved was inside another path it resolved, which
|
|
211
80
|
* cannot happen while the outermost one is always kept.
|
|
212
81
|
*/
|
|
213
|
-
function nothingToDo(say,
|
|
214
|
-
if (options.json) {
|
|
215
|
-
say(JSON.stringify({ paths: [], ids: [], files: 0, skipped, changed: false, reappliedAfterConflict: false, seq }));
|
|
216
|
-
return 0;
|
|
217
|
-
}
|
|
82
|
+
function nothingToDo(say, verb, skipped) {
|
|
218
83
|
const names = skipped.map((s) => `"${s}"`).join(", ");
|
|
219
84
|
say(verb === "restore"
|
|
220
85
|
? `${names} ${skipped.length === 1 ? "is" : "are"} not in the trash. Nothing changed.`
|
|
221
86
|
: `Nothing was left to move to the trash. Nothing changed.`);
|
|
222
87
|
return 0;
|
|
223
88
|
}
|
|
224
|
-
/** Is any ancestor of this entry in the set? Used to drop a target a named folder already covers. */
|
|
225
|
-
function hasNamedAncestor(entries, entry, named) {
|
|
226
|
-
const byId = buildIndex(entries).byId;
|
|
227
|
-
const seen = new Set([entry.id]);
|
|
228
|
-
let at = entry.parentId;
|
|
229
|
-
while (at !== null && !seen.has(at)) {
|
|
230
|
-
if (named.has(at))
|
|
231
|
-
return true;
|
|
232
|
-
seen.add(at);
|
|
233
|
-
at = byId.get(at)?.parentId ?? null;
|
|
234
|
-
}
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
/** One entry per id, keeping the first. Two named folders can hold the same file only once. */
|
|
238
|
-
function uniqueById(files) {
|
|
239
|
-
const byId = new Map();
|
|
240
|
-
for (const file of files)
|
|
241
|
-
if (!byId.has(file.id))
|
|
242
|
-
byId.set(file.id, file);
|
|
243
|
-
return [...byId.values()];
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Every file at or under one entry.
|
|
247
|
-
*
|
|
248
|
-
* ⚠ Trashed descendants are INCLUDED HERE, and the CALLER filters. Somebody who trashed one file
|
|
249
|
-
* last week and then trashes its folder expects the folder to be gone from the server too — so
|
|
250
|
-
* `rm` takes this set whole. `restore` cannot: see the note at the call site.
|
|
251
|
-
*/
|
|
252
|
-
export function filesUnder(entries, rootId) {
|
|
253
|
-
const root = entries.find((e) => e.id === rootId);
|
|
254
|
-
if (root === undefined)
|
|
255
|
-
return [];
|
|
256
|
-
if (root.kind === KIND_FILE)
|
|
257
|
-
return [root];
|
|
258
|
-
const childrenOf = new Map();
|
|
259
|
-
for (const e of entries) {
|
|
260
|
-
const list = childrenOf.get(e.parentId);
|
|
261
|
-
if (list === undefined)
|
|
262
|
-
childrenOf.set(e.parentId, [e]);
|
|
263
|
-
else
|
|
264
|
-
list.push(e);
|
|
265
|
-
}
|
|
266
|
-
const out = [];
|
|
267
|
-
const seen = new Set([rootId]);
|
|
268
|
-
const queue = [rootId];
|
|
269
|
-
while (queue.length > 0) {
|
|
270
|
-
const id = queue.pop();
|
|
271
|
-
if (id === undefined)
|
|
272
|
-
break;
|
|
273
|
-
for (const child of childrenOf.get(id) ?? []) {
|
|
274
|
-
if (seen.has(child.id))
|
|
275
|
-
continue;
|
|
276
|
-
seen.add(child.id);
|
|
277
|
-
if (child.kind === KIND_FILE)
|
|
278
|
-
out.push(child);
|
|
279
|
-
else
|
|
280
|
-
queue.push(child.id);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return out;
|
|
284
|
-
}
|
|
285
|
-
/** ⛔ 404 is "already in the state you asked for", which is what a half-finished run leaves. */
|
|
286
|
-
async function tellServer(base, apiKey, verb, id) {
|
|
287
|
-
await setTrashed(base, apiKey, id, verb === "rm");
|
|
288
|
-
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { NmtsError } from "./errors.ts";
|
|
2
|
+
import { type ListEditInput } from "./manifest-write.ts";
|
|
3
|
+
import type { ManifestEntry } from "./shared/lib/drive/manifest-codec.ts";
|
|
4
|
+
/**
|
|
5
|
+
* What went wrong, in a word a program can branch on.
|
|
6
|
+
*
|
|
7
|
+
* ⚠ FIVE, AND THEY ARE ABOUT THE LIST. A server refusal arrives as `ServerError` with the server's
|
|
8
|
+
* own code, and a lost compare-and-swap that never settles arrives as a plain `NmtsError`;
|
|
9
|
+
* neither is a decision this file made.
|
|
10
|
+
*/
|
|
11
|
+
export type DriveEditCode =
|
|
12
|
+
/** No entry at that path — or a path that names two, which is the same "which one?" */
|
|
13
|
+
"NOT_FOUND"
|
|
14
|
+
/** Something else in that folder already answers to that name. */
|
|
15
|
+
| "NAME_TAKEN"
|
|
16
|
+
/** The name itself cannot be used: empty, `.`, `..`, or a path pretending to be a name. */
|
|
17
|
+
| "BAD_NAME"
|
|
18
|
+
/** A restore was asked for something that is not in the trash, or not in it on its own account. */
|
|
19
|
+
| "NOT_IN_TRASH"
|
|
20
|
+
/** A folder was asked to move inside itself. */
|
|
21
|
+
| "INTO_ITSELF";
|
|
22
|
+
/** A refusal about the list, with the sentence a person reads and the word a program reads. */
|
|
23
|
+
export declare class DriveEditError extends NmtsError {
|
|
24
|
+
readonly code: DriveEditCode;
|
|
25
|
+
constructor(code: DriveEditCode, message: string, options?: {
|
|
26
|
+
exitCode?: number;
|
|
27
|
+
nextStep?: string | null;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A new name is a name and not a path.
|
|
32
|
+
*
|
|
33
|
+
* ⛔ REFUSED RATHER THAN SPLIT. A name with a `/` in it is somebody asking for a move while typing
|
|
34
|
+
* a rename, and quietly doing the move would put the file somewhere they did not look.
|
|
35
|
+
*/
|
|
36
|
+
export declare function requireNewName(name: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Make a folder path, and every folder above it that is missing, for an account already opened.
|
|
39
|
+
*
|
|
40
|
+
* ⛔ THE RULES BELOW ARE THE ONES A SECOND COPY WOULD GET SUBTLY WRONG: a folder that is already
|
|
41
|
+
* there IS the folder asked for (never a numbered one), the decision is taken inside each
|
|
42
|
+
* attempt so a lost race cannot make two, and what was made before a failure is named rather
|
|
43
|
+
* than silently kept.
|
|
44
|
+
*
|
|
45
|
+
* ⚠ MISSING PARENTS ARE CREATED, and that is a decision rather than a convenience. A folder costs
|
|
46
|
+
* nothing, holds nothing and can be moved to the trash, so the failure mode of creating one too
|
|
47
|
+
* many is a tidy-up; the failure mode of refusing is a caller that has to discover the tree one
|
|
48
|
+
* call at a time. Every folder made is named in the result, so it is never a surprise.
|
|
49
|
+
*/
|
|
50
|
+
export declare function ensureFolderPath(input: ListEditInput, wanted: string): Promise<{
|
|
51
|
+
parentId: string | null;
|
|
52
|
+
made: string[];
|
|
53
|
+
}>;
|
|
54
|
+
/** What making a folder did. `made` is empty when every folder in the path was already there. */
|
|
55
|
+
export interface MadeFolder {
|
|
56
|
+
/** The path as the drive spells it — no leading slash, no trailing one. */
|
|
57
|
+
path: string;
|
|
58
|
+
/** The folder at the end of the path. Null only for the top of the drive, which is never made. */
|
|
59
|
+
parentId: string | null;
|
|
60
|
+
/** The folders this call actually made, outermost first. */
|
|
61
|
+
made: string[];
|
|
62
|
+
}
|
|
63
|
+
/** Make one folder path. A path that is already there is a success with nothing made. */
|
|
64
|
+
export declare function makeFolder(input: ListEditInput, path: string): Promise<MadeFolder>;
|
|
65
|
+
/** One thing a move carried, with where it came from and where it landed. */
|
|
66
|
+
export interface MovedThing {
|
|
67
|
+
id: string;
|
|
68
|
+
/** Its name, which a move never changes. */
|
|
69
|
+
name: string;
|
|
70
|
+
/** Its full path before the move. */
|
|
71
|
+
from: string;
|
|
72
|
+
/** Its full path after, or null if another device took it out of the list meanwhile. */
|
|
73
|
+
path: string | null;
|
|
74
|
+
}
|
|
75
|
+
/** What one run of moving did. */
|
|
76
|
+
export interface MoveOutcome {
|
|
77
|
+
/** The things this run moved, in the order they were named. */
|
|
78
|
+
moved: MovedThing[];
|
|
79
|
+
/** The names that were already in the destination, so nothing was written for them. */
|
|
80
|
+
already: string[];
|
|
81
|
+
/** The destination folder id, or null for the top of the drive. */
|
|
82
|
+
parentId: string | null;
|
|
83
|
+
/** False when everything named was already there, so no list was written. */
|
|
84
|
+
changed: boolean;
|
|
85
|
+
/** True when the list was rebuilt because another device wrote first. */
|
|
86
|
+
reappliedAfterConflict: boolean;
|
|
87
|
+
/** The list version now current. */
|
|
88
|
+
seq: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Move things into a folder. An empty destination is the top of the drive.
|
|
92
|
+
*
|
|
93
|
+
* ⛔ ONE WRITE FOR THE WHOLE RUN, however many things are named. The list is rewritten whole on
|
|
94
|
+
* every save, so a second thing costs nothing extra — while a second WRITE is a second chance
|
|
95
|
+
* to lose the compare-and-swap, and losing it half way through a run leaves some things moved
|
|
96
|
+
* and some not, which is a state the caller cannot tell apart from the one it asked for.
|
|
97
|
+
*
|
|
98
|
+
* ⛔ AND THE NAME CHECK RUNS AGAINST WHAT THIS RUN HAS ALREADY MOVED, not against the list as it
|
|
99
|
+
* was read. Two files called `notes.txt` in two folders, moved into one folder by one call,
|
|
100
|
+
* would otherwise both be written — two entries at one path, which nothing can address
|
|
101
|
+
* afterwards: every lookup answers "names 2 things in this account". So the loop folds each
|
|
102
|
+
* move onto a working copy and asks the working copy the next question.
|
|
103
|
+
*/
|
|
104
|
+
export declare function moveEntries(input: ListEditInput, paths: readonly string[], destination: string): Promise<MoveOutcome>;
|
|
105
|
+
/** What renaming one thing did. */
|
|
106
|
+
export interface RenameOutcome {
|
|
107
|
+
id: string;
|
|
108
|
+
/** The name it had. */
|
|
109
|
+
from: string;
|
|
110
|
+
/** The full path it had, which is what a person recognises it by. */
|
|
111
|
+
fromPath: string;
|
|
112
|
+
/** The name it has now. */
|
|
113
|
+
to: string;
|
|
114
|
+
/** False when it was already called that, so no list was written. */
|
|
115
|
+
changed: boolean;
|
|
116
|
+
reappliedAfterConflict: boolean;
|
|
117
|
+
seq: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Give one thing a new name. The path stays the same otherwise.
|
|
121
|
+
*
|
|
122
|
+
* ⛔ REFUSED RATHER THAN NUMBERED, AND THE REFUSAL IS RE-DECIDED ON EVERY ATTEMPT. An upload picks
|
|
123
|
+
* `report (2).pdf` because nobody was watching; a rename is somebody choosing a name on purpose,
|
|
124
|
+
* and silently giving them a different one is how two files end up looking like a mistake nobody
|
|
125
|
+
* made. Checking once, before the write, was not enough: when another device took the name in
|
|
126
|
+
* between, the retry re-applied the old decision and produced two entries at one path, which
|
|
127
|
+
* nothing can address afterwards (2026-08-23).
|
|
128
|
+
*/
|
|
129
|
+
export declare function renameEntry(input: ListEditInput, path: string, name: string): Promise<RenameOutcome>;
|
|
130
|
+
/**
|
|
131
|
+
* What one run of the trash did — and, in this order, exactly what `nmts rm --json` prints.
|
|
132
|
+
*
|
|
133
|
+
* ⛔ THE ORDER OF THESE FIELDS IS THE COMMAND'S JSON. The command hands this object straight to
|
|
134
|
+
* `JSON.stringify`, so a field added in the middle changes what an agent reading that output
|
|
135
|
+
* sees. Add at the end, or not at all.
|
|
136
|
+
*/
|
|
137
|
+
export interface TrashOutcome {
|
|
138
|
+
/** The paths acted on. Empty when everything named was already where it was asked to be. */
|
|
139
|
+
paths: string[];
|
|
140
|
+
/** Their ids, in the same order. */
|
|
141
|
+
ids: string[];
|
|
142
|
+
/** How many server rows were moved. A folder has none of its own; its files have one each. */
|
|
143
|
+
files: number;
|
|
144
|
+
/** Named, and nothing written for them: already out of the trash, or covered by a named folder. */
|
|
145
|
+
skipped: string[];
|
|
146
|
+
changed: boolean;
|
|
147
|
+
reappliedAfterConflict: boolean;
|
|
148
|
+
seq: number;
|
|
149
|
+
}
|
|
150
|
+
export interface TrashEditOptions {
|
|
151
|
+
/**
|
|
152
|
+
* Refuse what the command-line tool names and carries on with.
|
|
153
|
+
*
|
|
154
|
+
* ⛔ OFF FOR THE COMMANDS AND ON FOR A LIBRARY, and the difference is who is reading. A person
|
|
155
|
+
* who typed `nmts restore a.txt b.txt` and had already restored `a.txt` wants `b.txt` back and
|
|
156
|
+
* a line saying the first was not in the trash; a program calling `restore` wants to know that
|
|
157
|
+
* what it asked for was not what it got, and the only way it learns that is a refusal.
|
|
158
|
+
*
|
|
159
|
+
* It adds two: a path that is not in the trash (`NOT_IN_TRASH`) and a restore whose old name has
|
|
160
|
+
* been taken since (`NAME_TAKEN`).
|
|
161
|
+
*/
|
|
162
|
+
strict?: boolean;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Move things to the trash, or bring them back.
|
|
166
|
+
*
|
|
167
|
+
* ⛔ NEITHER HALF DESTROYS ANYTHING. `rm` moves everything it is given to the trash, where it stays
|
|
168
|
+
* restorable for thirty days; the endpoint that erases a stored row for good is closed to an API
|
|
169
|
+
* key and stays closed, so nothing here can reach it.
|
|
170
|
+
*
|
|
171
|
+
* ⛔ THE SERVER ROW GOES FIRST, AND "ALREADY DONE" COUNTS AS DONE. A trashed item's bytes cannot be
|
|
172
|
+
* fetched, so the state to avoid above all others is a list that shows a file as live when the
|
|
173
|
+
* server has already trashed it: the person sees it, asks for it, and is told it does not exist.
|
|
174
|
+
* Writing the list only after the server agreed means a failed server call leaves the drive
|
|
175
|
+
* exactly as it was — the state a caller can act on.
|
|
176
|
+
*
|
|
177
|
+
* ⛔ AND ONE PATH THAT WILL NOT RESOLVE REFUSES THE WHOLE RUN, before a single server row is
|
|
178
|
+
* touched. Trashing four of the five things somebody named and answering success is worse than
|
|
179
|
+
* trashing none: the run reads as done, and finding the odd one out means diffing the drive.
|
|
180
|
+
*/
|
|
181
|
+
export declare function trashPaths(input: ListEditInput, verb: "rm" | "restore", paths: readonly string[], options?: TrashEditOptions): Promise<TrashOutcome>;
|
|
182
|
+
/**
|
|
183
|
+
* Every file at or under one entry.
|
|
184
|
+
*
|
|
185
|
+
* ⚠ Trashed descendants are INCLUDED HERE, and the CALLER filters. Somebody who trashed one file
|
|
186
|
+
* last week and then trashes its folder expects the folder to be gone from the server too — so
|
|
187
|
+
* `rm` takes this set whole. `restore` cannot: see the note at the call site.
|
|
188
|
+
*/
|
|
189
|
+
export declare function filesUnder(entries: readonly ManifestEntry[], rootId: string): ManifestEntry[];
|