@tinycloud/cli 1.0.0-beta.16 → 1.0.0-beta.17
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/README.md +24 -7
- package/dist/index.js +106 -45
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +1053 -361
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +7 -5
- package/skills/tc-cli/AUTH.md +88 -0
- package/skills/tc-cli/INSTALL.md +28 -0
- package/skills/tc-cli/REFERENCE.md +215 -0
- package/skills/tc-cli/SDK.md +58 -0
- package/skills/tc-cli/SKILL.md +69 -0
- package/skills/tc-cli/release.json +15 -0
package/README.md
CHANGED
|
@@ -8,7 +8,16 @@ Self-sovereign storage from the terminal. `tc` is the command-line interface for
|
|
|
8
8
|
npm install -g @tinycloud/cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Requires Node.js >= 20.
|
|
11
|
+
Requires Node.js >= 20. On Linux, `/usr/sbin/tc` (iproute2) can shadow this
|
|
12
|
+
CLI; call it as `"$(npm prefix --global)/bin/tc"` when in doubt.
|
|
13
|
+
|
|
14
|
+
## Agent skills
|
|
15
|
+
|
|
16
|
+
The package ships the general `tc-cli` skill and its references in
|
|
17
|
+
`skills/tc-cli`. See [skill installation](skills/tc-cli/INSTALL.md) for a
|
|
18
|
+
pinned, cross-agent installation with OpenCode, Codex and Claude Code, and
|
|
19
|
+
[authentication](skills/tc-cli/AUTH.md) for device login and relaying the
|
|
20
|
+
approval code to the owner.
|
|
12
21
|
|
|
13
22
|
## Operations coverage
|
|
14
23
|
|
|
@@ -34,11 +43,17 @@ tc init
|
|
|
34
43
|
# Authenticate via browser
|
|
35
44
|
tc auth login
|
|
36
45
|
|
|
37
|
-
# Authenticate on a remote/headless machine
|
|
38
|
-
|
|
46
|
+
# Authenticate on a remote/headless machine: the owner approves an explicit
|
|
47
|
+
# manifest on their phone (URL + code printed to stderr). Use a new profile
|
|
48
|
+
# name if it already exists; keep the user's existing profiles.
|
|
49
|
+
tc init --name publisher --key-only
|
|
50
|
+
tc --profile publisher auth login --device --manifest builtin:share-publishing
|
|
51
|
+
|
|
52
|
+
# Same Share publishing scope, as a shortcut (on the dedicated profile)
|
|
53
|
+
tc --profile publisher enable share
|
|
39
54
|
|
|
40
|
-
#
|
|
41
|
-
tc
|
|
55
|
+
# Selected profile, owner, host and space (JSON; does not test access)
|
|
56
|
+
tc context
|
|
42
57
|
|
|
43
58
|
# Store and retrieve data
|
|
44
59
|
tc kv put greeting "Hello, world"
|
|
@@ -64,8 +79,10 @@ tc delegation create --to did:pkh:eip155:1:0x...
|
|
|
64
79
|
|---------|-------------|
|
|
65
80
|
| `tc init` | Set up a profile and generate keys |
|
|
66
81
|
| `tc auth login` | Authenticate via browser |
|
|
67
|
-
| `tc auth login --device
|
|
68
|
-
| `tc
|
|
82
|
+
| `tc auth login --device --manifest <file>` | Approve exactly a manifest's permissions on another device (phone) |
|
|
83
|
+
| `tc auth login --manifest <file>` | Browser OpenKey login scoped to a manifest |
|
|
84
|
+
| `tc enable share` | Device login with the built-in Share publishing manifest (use a dedicated profile) |
|
|
85
|
+
| `tc context` | Show the selected profile, owner, host, space and session state |
|
|
69
86
|
| `tc auth status` | Show authentication status |
|
|
70
87
|
| `tc auth whoami` | Show current identity |
|
|
71
88
|
| `tc kv get <key>` | Retrieve a value |
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ var init_constants = __esm({
|
|
|
21
21
|
CONFIG_DIR = tinycloudHomePath();
|
|
22
22
|
PROFILES_DIR = profilesPath();
|
|
23
23
|
CONFIG_FILE = tinycloudConfigPath();
|
|
24
|
-
DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
24
|
+
DEFAULT_HOST = "https://tee.node.tinycloud.xyz";
|
|
25
25
|
DEFAULT_PROFILE = "default";
|
|
26
26
|
ExitCode = {
|
|
27
27
|
SUCCESS: 0,
|
|
@@ -104,6 +104,7 @@ var init_formatter = __esm({
|
|
|
104
104
|
// src/output/errors.ts
|
|
105
105
|
import { readFileSync, readdirSync } from "fs";
|
|
106
106
|
import { join } from "path";
|
|
107
|
+
import { ProfileLockTimeoutError } from "@tinycloud/operations/state";
|
|
107
108
|
function setActiveProfileName(name) {
|
|
108
109
|
activeProfileName = name;
|
|
109
110
|
}
|
|
@@ -121,6 +122,14 @@ function wrapError(error) {
|
|
|
121
122
|
);
|
|
122
123
|
}
|
|
123
124
|
if (error instanceof CLIError) return error;
|
|
125
|
+
if (error instanceof ProfileLockTimeoutError) {
|
|
126
|
+
return new CLIError(
|
|
127
|
+
"PROFILE_LOCK_TIMEOUT",
|
|
128
|
+
`${message} Another tc or MCP process held this profile's lock, so the change that needed it was not written.`,
|
|
129
|
+
ExitCode.ERROR,
|
|
130
|
+
{ hint: "Wait for the other command to finish and retry. A crashed process's lock is reclaimed automatically after 30 s." }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
124
133
|
if (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired")) {
|
|
125
134
|
return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
|
|
126
135
|
}
|
|
@@ -244,9 +253,9 @@ async function readJson(filePath) {
|
|
|
244
253
|
async function writeJson(filePath, data) {
|
|
245
254
|
const directory = dirname(filePath);
|
|
246
255
|
const tempPath = join2(directory, `.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
247
|
-
await mkdir(directory, { recursive: true });
|
|
256
|
+
await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
248
257
|
try {
|
|
249
|
-
await writeFile(tempPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
258
|
+
await writeFile(tempPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: PRIVATE_FILE_MODE });
|
|
250
259
|
await rename(tempPath, filePath);
|
|
251
260
|
} catch (err) {
|
|
252
261
|
await rm(tempPath, { force: true }).catch(() => void 0);
|
|
@@ -265,7 +274,7 @@ async function fileExists(filePath) {
|
|
|
265
274
|
}
|
|
266
275
|
}
|
|
267
276
|
async function ensureDir(dirPath) {
|
|
268
|
-
await mkdir(dirPath, { recursive: true });
|
|
277
|
+
await mkdir(dirPath, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
269
278
|
}
|
|
270
279
|
async function removeDir(dirPath) {
|
|
271
280
|
await rm(dirPath, { recursive: true, force: true });
|
|
@@ -281,9 +290,12 @@ async function listDirs(dirPath) {
|
|
|
281
290
|
throw err;
|
|
282
291
|
}
|
|
283
292
|
}
|
|
293
|
+
var PRIVATE_FILE_MODE, PRIVATE_DIR_MODE;
|
|
284
294
|
var init_storage = __esm({
|
|
285
295
|
"src/config/storage.ts"() {
|
|
286
296
|
"use strict";
|
|
297
|
+
PRIVATE_FILE_MODE = 384;
|
|
298
|
+
PRIVATE_DIR_MODE = 448;
|
|
287
299
|
}
|
|
288
300
|
});
|
|
289
301
|
|
|
@@ -419,17 +431,14 @@ function profileLocalNodeIdentityStore(profileName) {
|
|
|
419
431
|
return profile?.pinnedLocalNodeDids?.[url];
|
|
420
432
|
},
|
|
421
433
|
set: async (url, nodeDid) => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
);
|
|
425
|
-
if (!profile) return;
|
|
426
|
-
await ProfileManager.setProfile(profileName, {
|
|
434
|
+
if (!await ProfileManager.profileExists(profileName)) return;
|
|
435
|
+
await ProfileManager.updateProfile(profileName, (profile) => ({
|
|
427
436
|
...profile,
|
|
428
437
|
pinnedLocalNodeDids: {
|
|
429
438
|
...profile.pinnedLocalNodeDids,
|
|
430
439
|
[url]: nodeDid
|
|
431
440
|
}
|
|
432
|
-
});
|
|
441
|
+
}));
|
|
433
442
|
}
|
|
434
443
|
};
|
|
435
444
|
}
|
|
@@ -524,10 +533,12 @@ var init_host = __esm({
|
|
|
524
533
|
});
|
|
525
534
|
|
|
526
535
|
// src/config/profiles.ts
|
|
536
|
+
import { chmod, rm as rm2 } from "fs/promises";
|
|
527
537
|
import { join as join3 } from "path";
|
|
528
538
|
import {
|
|
529
539
|
readSession,
|
|
530
540
|
removeSession,
|
|
541
|
+
withProfileLock,
|
|
531
542
|
writeSession
|
|
532
543
|
} from "@tinycloud/operations/state";
|
|
533
544
|
var ProfileManager;
|
|
@@ -540,11 +551,41 @@ var init_profiles = __esm({
|
|
|
540
551
|
ProfileManager = class _ProfileManager {
|
|
541
552
|
// ── Initialization ──────────────────────────────────────────────────
|
|
542
553
|
/**
|
|
543
|
-
*
|
|
554
|
+
* Runs `action` holding the profile's store lock (shared with operations
|
|
555
|
+
* and MCP). Reentrant, so the writers below can be called inside it; hold
|
|
556
|
+
* it around a whole read-modify-write, not just each write.
|
|
557
|
+
*/
|
|
558
|
+
static async withLock(name, action, options) {
|
|
559
|
+
return withProfileLock(name, action, options);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Read-modify-write of profile.json under the profile lock, so concurrent
|
|
563
|
+
* updates (another command, a login commit) are never erased.
|
|
564
|
+
*/
|
|
565
|
+
static async updateProfile(name, update) {
|
|
566
|
+
return _ProfileManager.withLock(name, async () => {
|
|
567
|
+
const next = update(await _ProfileManager.getProfile(name));
|
|
568
|
+
await _ProfileManager.setProfile(name, next);
|
|
569
|
+
return next;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Creates ~/.tinycloud/ and ~/.tinycloud/profiles/ if they don't exist and
|
|
574
|
+
* (re)sets both to 0700: older releases created them 0775.
|
|
544
575
|
*/
|
|
545
576
|
static async ensureConfigDir() {
|
|
546
|
-
|
|
547
|
-
|
|
577
|
+
for (const directory of [CONFIG_DIR, PROFILES_DIR]) {
|
|
578
|
+
await ensureDir(directory);
|
|
579
|
+
await chmod(directory, PRIVATE_DIR_MODE);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/** Owner-only profile directory (0700), created or tightened before any write into it. */
|
|
583
|
+
static async ensureProfileDir(name) {
|
|
584
|
+
await _ProfileManager.ensureConfigDir();
|
|
585
|
+
const profileDir = join3(PROFILES_DIR, name);
|
|
586
|
+
await ensureDir(profileDir);
|
|
587
|
+
await chmod(profileDir, PRIVATE_DIR_MODE);
|
|
588
|
+
return profileDir;
|
|
548
589
|
}
|
|
549
590
|
// ── Global config ───────────────────────────────────────────────────
|
|
550
591
|
/**
|
|
@@ -581,12 +622,17 @@ var init_profiles = __esm({
|
|
|
581
622
|
return profile;
|
|
582
623
|
}
|
|
583
624
|
/**
|
|
584
|
-
* Saves a profile config
|
|
625
|
+
* Saves a profile config under the profile lock, creating the profile
|
|
626
|
+
* directory if needed. Use `updateProfile` to change an existing profile.
|
|
585
627
|
*/
|
|
586
628
|
static async setProfile(name, data) {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
629
|
+
await _ProfileManager.withLock(name, async () => {
|
|
630
|
+
await writeJson(join3(await _ProfileManager.ensureProfileDir(name), "profile.json"), data);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
/** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
|
|
634
|
+
static async removeProfileConfig(name) {
|
|
635
|
+
await _ProfileManager.withLock(name, () => rm2(join3(PROFILES_DIR, name, "profile.json"), { force: true }));
|
|
590
636
|
}
|
|
591
637
|
/**
|
|
592
638
|
* Returns true if a profile directory exists.
|
|
@@ -622,13 +668,15 @@ var init_profiles = __esm({
|
|
|
622
668
|
static async getKey(name) {
|
|
623
669
|
return readJson(join3(PROFILES_DIR, name, "key.json"));
|
|
624
670
|
}
|
|
625
|
-
/**
|
|
626
|
-
* Saves a JWK key for a profile.
|
|
627
|
-
*/
|
|
671
|
+
/** Saves a JWK key under the profile lock (0600, in an owner-only profile directory). */
|
|
628
672
|
static async setKey(name, jwk) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
673
|
+
await _ProfileManager.withLock(name, async () => {
|
|
674
|
+
await writeJson(join3(await _ProfileManager.ensureProfileDir(name), "key.json"), jwk);
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
/** Removes key.json under the profile lock (rollback of a key a failed login created). */
|
|
678
|
+
static async removeKey(name) {
|
|
679
|
+
await _ProfileManager.withLock(name, () => rm2(join3(PROFILES_DIR, name, "key.json"), { force: true }));
|
|
632
680
|
}
|
|
633
681
|
// ── Session management ──────────────────────────────────────────────
|
|
634
682
|
/**
|
|
@@ -651,11 +699,13 @@ var init_profiles = __esm({
|
|
|
651
699
|
}
|
|
652
700
|
// ── Cache management ────────────────────────────────────────────────
|
|
653
701
|
/**
|
|
654
|
-
* Returns the
|
|
702
|
+
* Returns the profile's cache directory (share history lives here),
|
|
703
|
+
* created or tightened to 0700.
|
|
655
704
|
*/
|
|
656
705
|
static async getCacheDir(name) {
|
|
657
|
-
const cacheDir = join3(
|
|
706
|
+
const cacheDir = join3(await _ProfileManager.ensureProfileDir(name), "cache");
|
|
658
707
|
await ensureDir(cacheDir);
|
|
708
|
+
await chmod(cacheDir, PRIVATE_DIR_MODE);
|
|
659
709
|
return cacheDir;
|
|
660
710
|
}
|
|
661
711
|
// ── Resolution helpers ──────────────────────────────────────────────
|
|
@@ -5535,8 +5585,15 @@ var init_zod = __esm({
|
|
|
5535
5585
|
}
|
|
5536
5586
|
});
|
|
5537
5587
|
|
|
5588
|
+
// src/share/publishing-manifest.ts
|
|
5589
|
+
var init_publishing_manifest = __esm({
|
|
5590
|
+
"src/share/publishing-manifest.ts"() {
|
|
5591
|
+
"use strict";
|
|
5592
|
+
}
|
|
5593
|
+
});
|
|
5594
|
+
|
|
5538
5595
|
// src/lib/permissions.ts
|
|
5539
|
-
import { appendFile, readFile as readFile3 } from "fs/promises";
|
|
5596
|
+
import { appendFile, chmod as chmod2, readFile as readFile3 } from "fs/promises";
|
|
5540
5597
|
import { join as join5 } from "path";
|
|
5541
5598
|
import {
|
|
5542
5599
|
buildPermissionRequestArtifact,
|
|
@@ -5549,7 +5606,7 @@ import {
|
|
|
5549
5606
|
readAdditionalDelegations,
|
|
5550
5607
|
readAuthRequests,
|
|
5551
5608
|
upsertProfileRecord,
|
|
5552
|
-
withProfileLock,
|
|
5609
|
+
withProfileLock as withProfileLock2,
|
|
5553
5610
|
writeJsonAtomic
|
|
5554
5611
|
} from "@tinycloud/operations/state";
|
|
5555
5612
|
async function loadAdditionalDelegations(profile) {
|
|
@@ -5579,6 +5636,7 @@ var init_permissions = __esm({
|
|
|
5579
5636
|
init_errors();
|
|
5580
5637
|
init_constants();
|
|
5581
5638
|
init_space();
|
|
5639
|
+
init_publishing_manifest();
|
|
5582
5640
|
init_types();
|
|
5583
5641
|
}
|
|
5584
5642
|
});
|
|
@@ -5694,17 +5752,19 @@ async function bootstrapDelegatedSession(ctx, delegation) {
|
|
|
5694
5752
|
}
|
|
5695
5753
|
const key = await ProfileManager.getKey(ctx.profile);
|
|
5696
5754
|
const jwk = signerJwkForProfile(ctx.profile, void 0, key);
|
|
5697
|
-
await ProfileManager.
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5755
|
+
await ProfileManager.withLock(ctx.profile, async () => {
|
|
5756
|
+
await ProfileManager.setSession(ctx.profile, {
|
|
5757
|
+
delegationHeader: delegation.delegationHeader,
|
|
5758
|
+
delegationCid: delegation.cid,
|
|
5759
|
+
spaceId: delegation.spaceId,
|
|
5760
|
+
jwk,
|
|
5761
|
+
verificationMethod: sessionDid
|
|
5762
|
+
});
|
|
5763
|
+
await ProfileManager.updateProfile(ctx.profile, (current) => ({
|
|
5764
|
+
...current,
|
|
5765
|
+
sessionDid,
|
|
5766
|
+
spaceId: delegation.spaceId
|
|
5767
|
+
}));
|
|
5708
5768
|
});
|
|
5709
5769
|
return createSDKInstance(ctx);
|
|
5710
5770
|
}
|
|
@@ -15030,7 +15090,7 @@ function receiveJson(result, path) {
|
|
|
15030
15090
|
|
|
15031
15091
|
// src/share/io.ts
|
|
15032
15092
|
import { constants } from "fs";
|
|
15033
|
-
import { lstat, mkdir as mkdir2, mkdtemp, open as open2, readFile as readFile2, realpath, stat as stat2, link, rename as rename2, rm as
|
|
15093
|
+
import { lstat, mkdir as mkdir2, mkdtemp, open as open2, readFile as readFile2, realpath, stat as stat2, link, rename as rename2, rm as rm3, unlink } from "fs/promises";
|
|
15034
15094
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
15035
15095
|
import { basename as basename2, join as join4, resolve, sep } from "path";
|
|
15036
15096
|
var MAX_SHARE_STDIN_BYTES = 100 * 1024 * 1024;
|
|
@@ -15144,7 +15204,7 @@ async function writeShareOutput(directory, filename, bytes, force) {
|
|
|
15144
15204
|
if (temporaryPath !== void 0) await unlink(temporaryPath);
|
|
15145
15205
|
} catch {
|
|
15146
15206
|
}
|
|
15147
|
-
await
|
|
15207
|
+
await rm3(stagingDirectory, { recursive: true, force: true });
|
|
15148
15208
|
await directoryHandle.close();
|
|
15149
15209
|
}
|
|
15150
15210
|
return join4(outputDirectory, safeName);
|
|
@@ -15187,23 +15247,24 @@ function shareCliError(error) {
|
|
|
15187
15247
|
const profileName = "profileName" in failure ? failure.profileName : void 0;
|
|
15188
15248
|
const localKey = "localKey" in failure && failure.localKey === true;
|
|
15189
15249
|
const profileHint = profileName === void 0 ? "" : `--profile ${profileName} `;
|
|
15190
|
-
const loginHint =
|
|
15250
|
+
const loginHint = localKey ? `\`tc ${profileHint}auth login --method local\`` : `\`tc ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`tc ${profileHint}enable share\`)`;
|
|
15191
15251
|
if (failure.kind === "owner-space-unresolved") {
|
|
15192
|
-
return new CLIError("AUTH_REQUIRED", `a valid signed TinyCloud session is required; run
|
|
15252
|
+
return new CLIError("AUTH_REQUIRED", `a valid signed TinyCloud session is required; run ${loginHint}`, 3);
|
|
15193
15253
|
}
|
|
15194
15254
|
if (failure.kind === "scope-denied") {
|
|
15195
15255
|
const requiredAction = failure.requiredAction === void 0 ? "" : ` (${failure.requiredAction})`;
|
|
15196
|
-
|
|
15256
|
+
const renew = localKey ? `renew it with ${loginHint} using the required capability` : `request the builtin:share-publishing scope with ${loginHint}`;
|
|
15257
|
+
return new CLIError("PERMISSION_DENIED", `the session lacks ${failure.capability} authority${requiredAction}; ${renew}`, 5);
|
|
15197
15258
|
}
|
|
15198
15259
|
if (failure.kind === "lifetime-exceeds-session") {
|
|
15199
15260
|
const expiresAt = failure.sessionExpiresAt.toISOString();
|
|
15200
15261
|
if (failure.reason === "session-too-close") {
|
|
15201
|
-
return new CLIError("AUTH_REQUIRED", `the signed session expires too soon (${expiresAt}); log in again with
|
|
15262
|
+
return new CLIError("AUTH_REQUIRED", `the signed session expires too soon (${expiresAt}); log in again with ${loginHint}`, 3);
|
|
15202
15263
|
}
|
|
15203
15264
|
if (failure.reason === "below-minimum") {
|
|
15204
15265
|
return new CLIError("SESSION_LIFETIME_EXCEEDED", "share expiry must be at least 60 seconds from now; use a longer --expires value", 2);
|
|
15205
15266
|
}
|
|
15206
|
-
return new CLIError("SESSION_LIFETIME_EXCEEDED", `requested share lifetime exceeds session expiry ${expiresAt}; use a shorter --expires value or renew the session with
|
|
15267
|
+
return new CLIError("SESSION_LIFETIME_EXCEEDED", `requested share lifetime exceeds session expiry ${expiresAt}; use a shorter --expires value or renew the session with ${loginHint}`, 2);
|
|
15207
15268
|
}
|
|
15208
15269
|
if (failure.kind === "origin-mismatch") {
|
|
15209
15270
|
return new CLIError("ORIGIN_MISMATCH", "share origin does not match the configured service", 2);
|