@needmoretruth/nmts-cli 0.36.3 → 0.38.1
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/AGENTS.md +22 -0
- package/CHANGELOG.md +25 -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/errors.d.ts +44 -0
- package/dist/drive-edit/errors.js +65 -0
- package/dist/drive-edit/folders.d.ts +29 -0
- package/dist/drive-edit/folders.js +98 -0
- package/dist/drive-edit/move.d.ts +66 -0
- package/dist/drive-edit/move.js +119 -0
- package/dist/drive-edit/trash.d.ts +53 -0
- package/dist/drive-edit/trash.js +190 -0
- package/dist/drive-edit/tree.d.ts +15 -0
- package/dist/drive-edit/tree.js +77 -0
- package/dist/drive-edit.d.ts +9 -0
- package/dist/drive-edit.js +23 -0
- package/dist/product.d.ts +1 -1
- package/dist/product.js +1 -1
- package/dist/s3/contract.d.ts +80 -0
- package/dist/s3/contract.js +3 -0
- 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/routes.d.ts +4 -0
- package/dist/s3/routes.js +249 -0
- package/dist/s3/server.d.ts +15 -53
- package/dist/s3/server.js +18 -222
- 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/AGENTS.md
CHANGED
|
@@ -40,6 +40,28 @@ for the NMTS key: it asks nothing and works in a container. The NMTS key from `N
|
|
|
40
40
|
readable through `docker inspect`, `/proc/<pid>/environ` and most CI logs, so that shape stops
|
|
41
41
|
once, for an unlock (`nmts help login`).
|
|
42
42
|
|
|
43
|
+
## What is built on it today
|
|
44
|
+
|
|
45
|
+
Each of these works with what is published now. None of it is a plan.
|
|
46
|
+
|
|
47
|
+
- **Storage for an agent.** Notes, datasets and build outputs that outlive a session or a machine,
|
|
48
|
+
encrypted before they leave it. This command does it, and so does [its MCP server](#if-your-client-speaks-mcp).
|
|
49
|
+
- **Encrypted storage inside someone's product.** The [SDK](https://github.com/needmoretruth/nmts-sdk)
|
|
50
|
+
opens an NMTS account for each person who uses a product and keeps that person's files in it. The
|
|
51
|
+
NMTS key is held by the person (the product cannot read the files) or by the product
|
|
52
|
+
(`Nmts.managed`), and every SDK method works the same either way. The
|
|
53
|
+
[terms](https://nmts.me/terms) provide for it in 3.7, for commercial products too.
|
|
54
|
+
- **An S3 endpoint.** `@needmoretruth/nmts-sdk/gateway` speaks S3 with a bucket and a key pair per
|
|
55
|
+
customer, so django-storages, Rails Active Storage, rclone and backup tools store into NMTS
|
|
56
|
+
unchanged.
|
|
57
|
+
- **Files that come back without NMTS.** [nmts-recovery](https://github.com/needmoretruth/nmts-recovery)
|
|
58
|
+
rebuilds an account's files from Walrus with the NMTS key and the recovery list the person saved.
|
|
59
|
+
No NMTS server takes part.
|
|
60
|
+
|
|
61
|
+
An account pays with credits or straight from its own wallet. From the wallet, WAL goes to Walrus
|
|
62
|
+
and SUI to the chain, and NMTS takes no fee. NMTS does not inspect or certify products built with the SDK: who holds the
|
|
63
|
+
keys in such a product is that product's own statement.
|
|
64
|
+
|
|
43
65
|
## What only a person can do, once
|
|
44
66
|
|
|
45
67
|
Four things need a person, all at the beginning. If you are being set up, hand this list back in full at once.
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,31 @@
|
|
|
3
3
|
Each version's entry is what changed for the person or the program using `nmts`. The product's own
|
|
4
4
|
update history, which covers the site and the server too, is at https://nmts.me/updates.
|
|
5
5
|
|
|
6
|
+
## 0.38.1 — 2026-09-20
|
|
7
|
+
|
|
8
|
+
- The folder and trash functions behind `@needmoretruth/nmts-cli/drive-edit` no longer import
|
|
9
|
+
`node:crypto`, so a browser bundle that reaches them builds again; the browser entry of `nmts-sdk`
|
|
10
|
+
does. Nothing a command prints has changed.
|
|
11
|
+
- `AGENTS.md` has a new section, "What is built on it today": storage for an agent, encrypted
|
|
12
|
+
storage inside a product through the SDK, the S3 endpoint, and recovery without NMTS.
|
|
13
|
+
|
|
14
|
+
## 0.38.0 — 2026-09-19
|
|
15
|
+
|
|
16
|
+
- The library surface has a new entry, `@needmoretruth/nmts-cli/s3-gateway`: the S3 server `nmts s3`
|
|
17
|
+
runs, taking a function that says which drive a bucket name means and a list of key pairs that
|
|
18
|
+
may each be held to named buckets, so a program can put it in front of more than one account and
|
|
19
|
+
mount it in a server of its own. `nmts s3` is unchanged: the same output, the same bucket, the same
|
|
20
|
+
refusals. `nmts-sdk` 0.4.0 is built on it.
|
|
21
|
+
|
|
22
|
+
## 0.37.0 — 2026-09-19
|
|
23
|
+
|
|
24
|
+
- The library surface has a new entry, `@needmoretruth/nmts-cli/drive-edit`: the list edits behind
|
|
25
|
+
`mkdir`, `mv`, `rename`, `rm` and `restore` as functions that print nothing and refuse with a
|
|
26
|
+
`code` (`NOT_FOUND`, `NAME_TAKEN`, `BAD_NAME`, `NOT_IN_TRASH`, `INTO_ITSELF`). The commands call the
|
|
27
|
+
same functions and print what they printed before. `nmts-sdk` 0.3.0 is built on it.
|
|
28
|
+
- Six command documents called the NMTS key "code", the name it had before; `nmts help extend`
|
|
29
|
+
carried an empty code block.
|
|
30
|
+
|
|
6
31
|
## 0.36.3 — 2026-09-19
|
|
7
32
|
|
|
8
33
|
- `--help` had the `--on-collision` lines in the middle of the `--part-size` description, so the
|
package/README.ko.md
CHANGED
|
@@ -50,7 +50,7 @@ nmts --help
|
|
|
50
50
|
|
|
51
51
|
```sh
|
|
52
52
|
npm install -g github:needmoretruth/nmts-cli # 기본 브랜치
|
|
53
|
-
npm install -g github:needmoretruth/nmts-cli#v0.
|
|
53
|
+
npm install -g github:needmoretruth/nmts-cli#v0.38.1 # 버전을 고정할 때
|
|
54
54
|
npm install -g https://github.com/needmoretruth/nmts-cli/releases/latest/download/nmts.tgz
|
|
55
55
|
```
|
|
56
56
|
|
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ default branch, from a pinned version, or from the tarball attached to the
|
|
|
51
51
|
|
|
52
52
|
```sh
|
|
53
53
|
npm install -g github:needmoretruth/nmts-cli # the default branch
|
|
54
|
-
npm install -g github:needmoretruth/nmts-cli#v0.
|
|
54
|
+
npm install -g github:needmoretruth/nmts-cli#v0.38.1 # a pinned version
|
|
55
55
|
npm install -g https://github.com/needmoretruth/nmts-cli/releases/latest/download/nmts.tgz
|
|
56
56
|
```
|
|
57
57
|
|
package/dist/artifact-about.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare const CRYPTO_SPEC_URL = "https://github.com/needmoretruth/nmts-re
|
|
|
11
11
|
* is the field's own contract — what the writer says about itself — and a person holding two
|
|
12
12
|
* copies of one account's artefacts can then tell which program made each.
|
|
13
13
|
*/
|
|
14
|
-
export declare const WRITTEN_BY = "nmts-cli 0.
|
|
14
|
+
export declare const WRITTEN_BY = "nmts-cli 0.38.1";
|
|
15
15
|
/** Which of the three artefacts a wrapper is. A reader holding several can sort them. */
|
|
16
16
|
export type ArtifactKind = "recovery-list" | "file-list" | "recovery-kit";
|
|
17
17
|
/**
|
package/dist/commands/erase.js
CHANGED
|
@@ -26,7 +26,7 @@ import { applyToList, batchTargets } from "../manifest-write.js";
|
|
|
26
26
|
import { BINARY_NAME } from "../product.js";
|
|
27
27
|
import { promptLine, stdinIsATerminal } from "../prompt.js";
|
|
28
28
|
import { openSession } from "../session.js";
|
|
29
|
-
import { filesUnder } from "
|
|
29
|
+
import { filesUnder } from "../drive-edit.js";
|
|
30
30
|
/** The server takes at most this many ids in one erase (`ERASE_BATCH_MAX`). */
|
|
31
31
|
const BATCH = 200;
|
|
32
32
|
/** The refusal a no-deposit release gets when the balance cannot cover the doubled fee. */
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { openSession } from "../session.ts";
|
|
2
1
|
export interface OrganiseOptions {
|
|
3
2
|
server?: string | undefined;
|
|
4
3
|
network?: string | undefined;
|
|
@@ -8,38 +7,17 @@ export interface OrganiseOptions {
|
|
|
8
7
|
/**
|
|
9
8
|
* Make a folder, and any folder above it that is missing.
|
|
10
9
|
*
|
|
11
|
-
* ⚠ MISSING PARENTS ARE CREATED, and that is a decision rather than a convenience
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* command at a time. Every folder made is named in the output, so it is never a surprise.
|
|
10
|
+
* ⚠ MISSING PARENTS ARE CREATED, and that is a decision rather than a convenience — the reason is
|
|
11
|
+
* beside the code that does it, in `drive-edit.ts`. Every folder made is named in the output, so
|
|
12
|
+
* it is never a surprise.
|
|
15
13
|
*/
|
|
16
|
-
/**
|
|
17
|
-
* Make a folder path, and every folder above it that is missing, under an OPEN session.
|
|
18
|
-
*
|
|
19
|
-
* ⛔ SPLIT OUT SO AN UPLOAD OF A WHOLE DIRECTORY CAN USE IT. The rules below are the ones a second
|
|
20
|
-
* copy would get subtly wrong: a folder that is already there IS the folder asked for (never a
|
|
21
|
-
* numbered one), the decision is taken inside each attempt so a lost race cannot make two, and
|
|
22
|
-
* what was made before a failure is named rather than silently kept.
|
|
23
|
-
*/
|
|
24
|
-
export declare function ensureFolderPath(session: Awaited<ReturnType<typeof openSession>>, wanted: string): Promise<{
|
|
25
|
-
parentId: string | null;
|
|
26
|
-
made: string[];
|
|
27
|
-
}>;
|
|
28
14
|
export declare function mkdir(path: string | undefined, options?: OrganiseOptions): Promise<number>;
|
|
29
15
|
/**
|
|
30
16
|
* Move things into a folder. Every operand but the last is something to move; the last is where
|
|
31
17
|
* they go, and an empty one means the top of the drive.
|
|
32
18
|
*
|
|
33
|
-
* ⛔ ONE WRITE FOR THE WHOLE RUN, however many things are named
|
|
34
|
-
*
|
|
35
|
-
* to lose the compare-and-swap, and losing it half way through a run leaves some things moved
|
|
36
|
-
* and some not, which is a state the caller cannot tell apart from the one it asked for.
|
|
37
|
-
*
|
|
38
|
-
* ⛔ AND THE NAME CHECK RUNS AGAINST WHAT THIS RUN HAS ALREADY MOVED, not against the list as it
|
|
39
|
-
* was read. Two files called `notes.txt` in two folders, moved into one folder by one command,
|
|
40
|
-
* would otherwise both be written — two entries at one path, which no command in this tool can
|
|
41
|
-
* address afterwards: every one of them answers "names 2 things in this account". So the loop
|
|
42
|
-
* folds each move onto a working copy and asks the working copy the next question.
|
|
19
|
+
* ⛔ ONE WRITE FOR THE WHOLE RUN, however many things are named, and every guard is re-decided
|
|
20
|
+
* inside the attempt. Both reasons are in `drive-edit.ts`, beside the loop that keeps them.
|
|
43
21
|
*/
|
|
44
22
|
export declare function mv(operands: readonly string[], options?: OrganiseOptions): Promise<number>;
|
|
45
23
|
/** Give one thing a new name. The path stays the same otherwise. */
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// `nmts mkdir`, `nmts mv` and `nmts rename` — the three edits that touch no bytes.
|
|
2
2
|
//
|
|
3
|
+
// ⛔ WHAT THEY DO IS IN `drive-edit.ts`; WHAT IS HERE IS THE TERMINAL. The list editing moved out
|
|
4
|
+
// the day the SDK needed the same three verbs without one — one implementation, two callers, so
|
|
5
|
+
// "what does moving onto a taken name do" is answered in a single place.
|
|
6
|
+
//
|
|
3
7
|
// ⛔ NONE OF THESE ASKS THE SERVER TO CHANGE ANYTHING BUT THE SEALED LIST. A folder has no server
|
|
4
8
|
// row at all, and a name and a parent live only inside the list — the server holds an id, a
|
|
5
9
|
// size and a time, and was deliberately never given a place to put a name
|
|
@@ -10,97 +14,18 @@
|
|
|
10
14
|
//
|
|
11
15
|
// ⛔ AND NONE OF THEM STOPS TO ASK. Renaming, moving and making a folder are reversible and cost
|
|
12
16
|
// nothing, which is the whole test for whether this tool interrupts somebody. It does not.
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
17
|
+
import { makeFolder, moveEntries, renameEntry, requireNewName } from "../drive-edit.js";
|
|
18
|
+
import { normalisePath } from "../drive-paths.js";
|
|
15
19
|
import { NmtsError } from "../errors.js";
|
|
16
|
-
import { readFileList } from "../manifest.js";
|
|
17
|
-
import { applyManyToList, applyToList, batchTargets } from "../manifest-write.js";
|
|
18
20
|
import { openSession } from "../session.js";
|
|
19
|
-
import { applyIntent } from "../shared/lib/drive/manifest-ops.js";
|
|
20
21
|
const say_ = (line) => void process.stdout.write(`${line}\n`);
|
|
21
22
|
/**
|
|
22
23
|
* Make a folder, and any folder above it that is missing.
|
|
23
24
|
*
|
|
24
|
-
* ⚠ MISSING PARENTS ARE CREATED, and that is a decision rather than a convenience
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* command at a time. Every folder made is named in the output, so it is never a surprise.
|
|
28
|
-
*/
|
|
29
|
-
/**
|
|
30
|
-
* Make a folder path, and every folder above it that is missing, under an OPEN session.
|
|
31
|
-
*
|
|
32
|
-
* ⛔ SPLIT OUT SO AN UPLOAD OF A WHOLE DIRECTORY CAN USE IT. The rules below are the ones a second
|
|
33
|
-
* copy would get subtly wrong: a folder that is already there IS the folder asked for (never a
|
|
34
|
-
* numbered one), the decision is taken inside each attempt so a lost race cannot make two, and
|
|
35
|
-
* what was made before a failure is named rather than silently kept.
|
|
25
|
+
* ⚠ MISSING PARENTS ARE CREATED, and that is a decision rather than a convenience — the reason is
|
|
26
|
+
* beside the code that does it, in `drive-edit.ts`. Every folder made is named in the output, so
|
|
27
|
+
* it is never a surprise.
|
|
36
28
|
*/
|
|
37
|
-
export async function ensureFolderPath(session, wanted) {
|
|
38
|
-
const made = [];
|
|
39
|
-
let parentId = null;
|
|
40
|
-
let walked = "";
|
|
41
|
-
for (const name of wanted.split("/")) {
|
|
42
|
-
// ⚠ A name that is only spaces is refused too. `mkdir` used to accept it, and then `rm` and
|
|
43
|
-
// `restore` rejected the very path `ls` printed for it as "no path given" — a code-2 message
|
|
44
|
-
// blaming the caller for an argument they had supplied (2026-08-23).
|
|
45
|
-
if (name.trim() === "" || name === "." || name === "..") {
|
|
46
|
-
throw new NmtsError(`"${wanted}" is not a folder path this tool will make.`, {
|
|
47
|
-
exitCode: 2,
|
|
48
|
-
nextStep: `Empty names, "." and ".." are not folder names in a drive. Nothing was made.`,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
walked = walked === "" ? name : `${walked}/${name}`;
|
|
52
|
-
const under = parentId;
|
|
53
|
-
const here = walked;
|
|
54
|
-
const fresh = randomUUID();
|
|
55
|
-
let landedOn = fresh;
|
|
56
|
-
// ⛔ ONE WRITE PER FOLDER, and the check that decides whether to write happens INSIDE the
|
|
57
|
-
// attempt. Two things went wrong when it sat outside (2026-08-23):
|
|
58
|
-
// · running `mkdir` twice at the same moment made `shared` AND `shared (2)`, because the
|
|
59
|
-
// loser of the compare-and-swap re-applied a decision taken against the older list;
|
|
60
|
-
// · a trashed folder of the same name made the second `mkdir` produce `photos (2)` while
|
|
61
|
-
// printing `Made "photos"`, because it went through the upload helper — and picking a
|
|
62
|
-
// free name is the right rule for BYTES and the wrong rule for a folder. A folder with
|
|
63
|
-
// that name in that parent IS the folder that was asked for.
|
|
64
|
-
// Building the whole chain in memory and writing once would be fewer round trips and would
|
|
65
|
-
// also mean a lost compare-and-swap threw away folders the ones below already point at.
|
|
66
|
-
const result = await applyToList(session, (entries) => {
|
|
67
|
-
const there = entries.find((e) => e.parentId === under &&
|
|
68
|
-
normaliseName(e.name) === normaliseName(name) &&
|
|
69
|
-
e.deletedAt === undefined);
|
|
70
|
-
if (there !== undefined) {
|
|
71
|
-
if (there.kind !== KIND_FOLDER) {
|
|
72
|
-
throw new NmtsError(`"${here}" is a file, so nothing can be made inside it.`, {
|
|
73
|
-
exitCode: 4,
|
|
74
|
-
nextStep: made.length > 0 ? `The folders made so far are kept: ${made.join(", ")}.` : "Nothing was made.",
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
landedOn = there.id;
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
landedOn = fresh;
|
|
81
|
-
const at = Date.now();
|
|
82
|
-
return {
|
|
83
|
-
op: "add",
|
|
84
|
-
entry: { id: fresh, parentId: under, kind: KIND_FOLDER, name, size: 0, createdAt: at, updatedAt: at },
|
|
85
|
-
};
|
|
86
|
-
}).catch((error) => {
|
|
87
|
-
// ⛔ WHAT SURVIVED IS NAMED. A run that stops half way leaves real folders behind, and the
|
|
88
|
-
// message that says so was attached only to the "that is a file" refusal.
|
|
89
|
-
if (error instanceof NmtsError || made.length === 0)
|
|
90
|
-
throw error;
|
|
91
|
-
const because = error instanceof Error ? error.message : "the server refused";
|
|
92
|
-
throw new NmtsError(because, {
|
|
93
|
-
exitCode: 1,
|
|
94
|
-
nextStep: `The folders made so far are kept: ${made.join(", ")}. Running the same command again ` +
|
|
95
|
-
`makes the rest — nothing is lost.`,
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
if (result.changed)
|
|
99
|
-
made.push(here);
|
|
100
|
-
parentId = landedOn;
|
|
101
|
-
}
|
|
102
|
-
return { parentId, made };
|
|
103
|
-
}
|
|
104
29
|
export async function mkdir(path, options = {}) {
|
|
105
30
|
const say = options.write ?? say_;
|
|
106
31
|
const wanted = normalisePath(path ?? "");
|
|
@@ -108,7 +33,7 @@ export async function mkdir(path, options = {}) {
|
|
|
108
33
|
throw new NmtsError("`nmts mkdir` needs the path of the folder to make.", { exitCode: 2 });
|
|
109
34
|
}
|
|
110
35
|
const session = await openSession(options);
|
|
111
|
-
const { parentId, made } = await
|
|
36
|
+
const { parentId, made } = await makeFolder(session, wanted);
|
|
112
37
|
if (options.json) {
|
|
113
38
|
say(JSON.stringify({ path: wanted, id: parentId, made }));
|
|
114
39
|
return 0;
|
|
@@ -125,16 +50,8 @@ export async function mkdir(path, options = {}) {
|
|
|
125
50
|
* Move things into a folder. Every operand but the last is something to move; the last is where
|
|
126
51
|
* they go, and an empty one means the top of the drive.
|
|
127
52
|
*
|
|
128
|
-
* ⛔ ONE WRITE FOR THE WHOLE RUN, however many things are named
|
|
129
|
-
*
|
|
130
|
-
* to lose the compare-and-swap, and losing it half way through a run leaves some things moved
|
|
131
|
-
* and some not, which is a state the caller cannot tell apart from the one it asked for.
|
|
132
|
-
*
|
|
133
|
-
* ⛔ AND THE NAME CHECK RUNS AGAINST WHAT THIS RUN HAS ALREADY MOVED, not against the list as it
|
|
134
|
-
* was read. Two files called `notes.txt` in two folders, moved into one folder by one command,
|
|
135
|
-
* would otherwise both be written — two entries at one path, which no command in this tool can
|
|
136
|
-
* address afterwards: every one of them answers "names 2 things in this account". So the loop
|
|
137
|
-
* folds each move onto a working copy and asks the working copy the next question.
|
|
53
|
+
* ⛔ ONE WRITE FOR THE WHOLE RUN, however many things are named, and every guard is re-decided
|
|
54
|
+
* inside the attempt. Both reasons are in `drive-edit.ts`, beside the loop that keeps them.
|
|
138
55
|
*/
|
|
139
56
|
export async function mv(operands, options = {}) {
|
|
140
57
|
const say = options.write ?? say_;
|
|
@@ -148,60 +65,17 @@ export async function mv(operands, options = {}) {
|
|
|
148
65
|
});
|
|
149
66
|
}
|
|
150
67
|
const session = await openSession(options);
|
|
151
|
-
const
|
|
152
|
-
const outcome = { moved: [], already: [], parentId: null };
|
|
153
|
-
// ⛔ EVERY GUARD RUNS INSIDE THE ATTEMPT, INCLUDING WHICH ENTRY EACH PATH NAMES. A lost
|
|
154
|
-
// compare-and-swap re-applies the intent to a list that changed underneath — and when the
|
|
155
|
-
// winner had just taken this name, the loser landed on top of it and produced two entries at
|
|
156
|
-
// one path. Meanwhile the tool exited 0 and said the move had been made.
|
|
157
|
-
const result = await applyManyToList(session, (now) => {
|
|
158
|
-
const targets = batchTargets(now, paths, { nothingHappened: "Nothing was moved." });
|
|
159
|
-
const into = folderIdFor(destination, now, "Nothing was moved.");
|
|
160
|
-
const intents = [];
|
|
161
|
-
const moved = [];
|
|
162
|
-
const already = [];
|
|
163
|
-
let working = now;
|
|
164
|
-
for (const target of targets) {
|
|
165
|
-
if (into !== null && (into === target.id || isUnder(working, into, target.id))) {
|
|
166
|
-
throw new NmtsError(`A folder cannot be moved inside itself.`, {
|
|
167
|
-
exitCode: 4,
|
|
168
|
-
nextStep: "Nothing was moved.",
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
if (into === target.parentId) {
|
|
172
|
-
already.push(target.name);
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (namesIn(working, into).has(normaliseName(target.name))) {
|
|
176
|
-
throw new NmtsError(`Something called "${target.name}" is already in that folder.`, {
|
|
177
|
-
exitCode: 4,
|
|
178
|
-
nextStep: `Nothing was moved. Rename it first: nmts rename "${target.name}" <new name>`,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
const intent = { op: "move", id: target.id, parentId: into, at };
|
|
182
|
-
intents.push(intent);
|
|
183
|
-
working = applyIntent(working, intent);
|
|
184
|
-
moved.push({ id: target.id, name: target.name });
|
|
185
|
-
}
|
|
186
|
-
outcome.moved = moved;
|
|
187
|
-
outcome.already = already;
|
|
188
|
-
outcome.parentId = into;
|
|
189
|
-
return intents;
|
|
190
|
-
});
|
|
68
|
+
const outcome = await moveEntries(session, paths, destination);
|
|
191
69
|
const shownDestination = normalisePath(destination);
|
|
192
70
|
const where = shownDestination === "" ? "the top of the drive" : `"${shownDestination}"`;
|
|
193
71
|
if (options.json) {
|
|
194
|
-
const index = buildIndex(result.entries);
|
|
195
72
|
say(JSON.stringify({
|
|
196
|
-
moved: outcome.moved.map((m) => {
|
|
197
|
-
const live = result.entries.find((e) => e.id === m.id);
|
|
198
|
-
return { id: m.id, name: m.name, path: live === undefined ? null : fullPathOf(index, live) };
|
|
199
|
-
}),
|
|
73
|
+
moved: outcome.moved.map((m) => ({ id: m.id, name: m.name, path: m.path })),
|
|
200
74
|
already: outcome.already,
|
|
201
75
|
parentId: outcome.parentId,
|
|
202
|
-
changed:
|
|
203
|
-
reappliedAfterConflict:
|
|
204
|
-
seq:
|
|
76
|
+
changed: outcome.changed,
|
|
77
|
+
reappliedAfterConflict: outcome.reappliedAfterConflict,
|
|
78
|
+
seq: outcome.seq,
|
|
205
79
|
}));
|
|
206
80
|
return 0;
|
|
207
81
|
}
|
|
@@ -214,7 +88,7 @@ export async function mv(operands, options = {}) {
|
|
|
214
88
|
if (outcome.moved.length > 0 && outcome.already.length > 0) {
|
|
215
89
|
say(` ${outcome.already.map((n) => `"${n}"`).join(", ")} was already there.`);
|
|
216
90
|
}
|
|
217
|
-
if (
|
|
91
|
+
if (outcome.changed && outcome.reappliedAfterConflict) {
|
|
218
92
|
say(` Another device wrote the file list first, so this was applied to that version.`);
|
|
219
93
|
}
|
|
220
94
|
return 0;
|
|
@@ -228,61 +102,27 @@ export async function rename(path, name, options = {}) {
|
|
|
228
102
|
nextStep: `For example: nmts rename notes.txt "meeting notes.txt"`,
|
|
229
103
|
});
|
|
230
104
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
105
|
+
// ⛔ ASKED HERE AS WELL AS INSIDE, AND IT IS THE SAME FUNCTION RATHER THAN A SECOND COPY. A name
|
|
106
|
+
// with a `/` in it is wrong about the ARGUMENT, so it is refused before a session is opened —
|
|
107
|
+
// otherwise a machine with no API key would be told about the key instead of about the name
|
|
108
|
+
// that was typed.
|
|
109
|
+
requireNewName(name);
|
|
237
110
|
const session = await openSession(options);
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// ⛔ REFUSED RATHER THAN NUMBERED, AND THE REFUSAL IS RE-DECIDED ON EVERY ATTEMPT. An upload
|
|
244
|
-
// picks `report (2).pdf` because nobody was watching; a rename is somebody typing a name on
|
|
245
|
-
// purpose, and silently giving them a different one is how two files end up looking like a
|
|
246
|
-
// mistake nobody made. Checking once, before the write, was not enough: when another device
|
|
247
|
-
// took the name in between, the retry re-applied the old decision and produced two entries at
|
|
248
|
-
// one path, which no command in this tool can address afterwards (2026-08-23).
|
|
249
|
-
const result = await applyToList(session, (now) => {
|
|
250
|
-
const live = now.find((e) => e.id === target.id);
|
|
251
|
-
if (live === undefined)
|
|
252
|
-
return null;
|
|
253
|
-
if (normaliseName(name) !== normaliseName(live.name) && namesIn(now, live.parentId).has(normaliseName(name))) {
|
|
254
|
-
throw new NmtsError(`Something called "${name}" is already in that folder.`, {
|
|
255
|
-
exitCode: 4,
|
|
256
|
-
nextStep: "Nothing was renamed.",
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
return { op: "rename", id: target.id, name, at };
|
|
260
|
-
});
|
|
261
|
-
return report(say, options, result.changed, result.reappliedAfterConflict, {
|
|
262
|
-
text: result.changed ? `Renamed "${before}" to "${name}".` : `"${before}" is already called that.`,
|
|
111
|
+
const outcome = await renameEntry(session, path, name);
|
|
112
|
+
return report(say, options, outcome.changed, outcome.reappliedAfterConflict, {
|
|
113
|
+
text: outcome.changed
|
|
114
|
+
? `Renamed "${outcome.fromPath}" to "${name}".`
|
|
115
|
+
: `"${outcome.fromPath}" is already called that.`,
|
|
263
116
|
json: {
|
|
264
|
-
id:
|
|
265
|
-
from:
|
|
117
|
+
id: outcome.id,
|
|
118
|
+
from: outcome.from,
|
|
266
119
|
to: name,
|
|
267
|
-
changed:
|
|
268
|
-
reappliedAfterConflict:
|
|
269
|
-
seq:
|
|
120
|
+
changed: outcome.changed,
|
|
121
|
+
reappliedAfterConflict: outcome.reappliedAfterConflict,
|
|
122
|
+
seq: outcome.seq,
|
|
270
123
|
},
|
|
271
124
|
});
|
|
272
125
|
}
|
|
273
|
-
/** Is `id` at or under `rootId`? Used to refuse moving a folder into its own subtree. */
|
|
274
|
-
function isUnder(entries, id, rootId) {
|
|
275
|
-
const byId = buildIndex(entries).byId;
|
|
276
|
-
const seen = new Set();
|
|
277
|
-
let at = id;
|
|
278
|
-
while (at !== null && !seen.has(at)) {
|
|
279
|
-
if (at === rootId)
|
|
280
|
-
return true;
|
|
281
|
-
seen.add(at);
|
|
282
|
-
at = byId.get(at)?.parentId ?? null;
|
|
283
|
-
}
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
126
|
function report(say, options, changed, conflicted, out) {
|
|
287
127
|
if (options.json) {
|
|
288
128
|
say(JSON.stringify(out.json));
|
package/dist/commands/push.js
CHANGED
|
@@ -36,7 +36,7 @@ import { createUploadApi } from "../upload-api.js";
|
|
|
36
36
|
import { clearItemRecord, clearReservation } from "../upload-store.js";
|
|
37
37
|
import { CREDIT_BYTES, partSizeFor, planAndPrice, UPLOAD_EPOCHS } from "../upload-price.js";
|
|
38
38
|
import { createBlobProtocol, readCurrentEpoch } from "../walrus-write.js";
|
|
39
|
-
import { ensureFolderPath } from "
|
|
39
|
+
import { ensureFolderPath } from "../drive-edit.js";
|
|
40
40
|
import { payerOf, refuseWalletOnlyOptions } from "./put.js";
|
|
41
41
|
export async function push(target, options = {}) {
|
|
42
42
|
const say = options.write ?? ((line) => process.stdout.write(`${line}\n`));
|
package/dist/commands/s3.d.ts
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
/** MinIO's port, which is what most S3 tools already have in their examples. */
|
|
2
2
|
export declare const DEFAULT_PORT = 9000;
|
|
3
|
-
/**
|
|
4
|
-
* How long a file list may be reused before it is fetched again.
|
|
5
|
-
*
|
|
6
|
-
* ⛔ THERE IS A CACHE BECAUSE A SYNC IS THOUSANDS OF REQUESTS. Reading the list per request would
|
|
7
|
-
* mean a server round trip and a decryption for each one, so a listing of a large drive would
|
|
8
|
-
* take minutes and cost the account's rate budget. ⚠ It also means a file uploaded from another
|
|
9
|
-
* device can be up to this long in appearing here, which is the trade and is written in the
|
|
10
|
-
* tool's own words when it starts.
|
|
11
|
-
*/
|
|
12
|
-
export declare const LIST_CACHE_MS = 5000;
|
|
13
3
|
export interface S3Options {
|
|
14
4
|
server?: string | undefined;
|
|
15
5
|
network?: string | undefined;
|