@tinycloud/cli 1.2.6-beta.3 → 1.3.0-beta.5
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 +10 -3
- package/dist/index.js +49 -28
- package/dist/index.js.map +1 -1
- package/dist/legacy-entry.js +1284 -679
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +6 -6
- package/skills/tc-cli/REFERENCE.md +11 -5
package/README.md
CHANGED
|
@@ -171,9 +171,16 @@ whole space. Whole-space replication requires `kv-sync-root` support and
|
|
|
171
171
|
`--replication-allow-secrets`, because it includes `vault/`. Replication needs
|
|
172
172
|
unrestricted `get` and explicit `sync` on that folder or broader; an exact-key
|
|
173
173
|
grant never qualifies. Bare replica prefixes are rejected; use `notes/`.
|
|
174
|
-
Use `tc replica report` for
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
Use `tc replica report` for read-only aggregates of persisted replication events
|
|
175
|
+
and identity partitions. Live replica status is unavailable there. Report
|
|
176
|
+
`--purge` and `--clear-pending` refuse with exit 2 `REPORT_READ_ONLY`.
|
|
177
|
+
`tc replica reset --replica <name> --purge` is a partial alternative for a
|
|
178
|
+
named local replica only.
|
|
179
|
+
Flag-owned purge and pending clearing have no equivalent yet (TC-1309).
|
|
180
|
+
On macOS and Windows, `replica reset --purge` and `auth logout` with a legacy
|
|
181
|
+
`replicas/` tree refuse local removal rather than delete through an unpinned
|
|
182
|
+
path; leave that tree intact and retry on a supported platform.
|
|
183
|
+
`auth logout` removes both legacy replicas and the flag-owned
|
|
177
184
|
`replication/` root; `auth logout --keep-replicas` keeps both. B-int release
|
|
178
185
|
acceptance must verify the OpenKey `tc auth request --grant` fallback against
|
|
179
186
|
a real OpenKey using a test account and no production writes: approve the
|
package/dist/index.js
CHANGED
|
@@ -382,7 +382,7 @@ var init_storage = __esm({
|
|
|
382
382
|
// src/output/errors.ts
|
|
383
383
|
import { readFileSync, readdirSync } from "fs";
|
|
384
384
|
import { join } from "path";
|
|
385
|
-
import { ProfileDeletedError, ProfileLockTimeoutError } from "@tinycloud/operations/state";
|
|
385
|
+
import { ProfileDeletedError, ProfileLockTimeoutError, ProfileSymlinkError } from "@tinycloud/operations/state";
|
|
386
386
|
import { isSafeRecovery } from "@tinycloud/operations/recovery";
|
|
387
387
|
import { CLI_NAME as CLI_NAME3 } from "@tinycloud/operations/cli-name";
|
|
388
388
|
import {
|
|
@@ -421,6 +421,9 @@ function wrapError(error) {
|
|
|
421
421
|
if (error instanceof ProfileDeletedError) {
|
|
422
422
|
return new CLIError("PROFILE_NOT_FOUND", message);
|
|
423
423
|
}
|
|
424
|
+
if (error instanceof ProfileSymlinkError) {
|
|
425
|
+
return new CLIError("PROFILE_SYMLINK", message, ExitCode.ERROR);
|
|
426
|
+
}
|
|
424
427
|
if (verdict === void 0 && (message.includes("Not signed in") || message.includes("AUTH_EXPIRED") || message.includes("Session expired"))) {
|
|
425
428
|
return new CLIError("AUTH_REQUIRED", message, ExitCode.AUTH_REQUIRED);
|
|
426
429
|
}
|
|
@@ -5649,12 +5652,14 @@ import { resolveSecretsOwner } from "@tinycloud/operations/secret-request";
|
|
|
5649
5652
|
import {
|
|
5650
5653
|
additionalDelegationsPath as sharedAdditionalDelegationsPath,
|
|
5651
5654
|
authRequestsPath as sharedAuthRequestsPath,
|
|
5655
|
+
profilePath,
|
|
5652
5656
|
profileStoreMetadataPath,
|
|
5653
5657
|
readAdditionalDelegations,
|
|
5654
5658
|
readAuthRequests,
|
|
5655
5659
|
refuseWriteToDeletedProfile,
|
|
5656
5660
|
updateProfileStore,
|
|
5657
5661
|
withProfileLock,
|
|
5662
|
+
withPinnedProfileDirectory,
|
|
5658
5663
|
writeJsonAtomic
|
|
5659
5664
|
} from "@tinycloud/operations/state";
|
|
5660
5665
|
async function replayAdditionalDelegations(node, profile, options) {
|
|
@@ -5669,7 +5674,6 @@ async function replayAdditionalDelegations(node, profile, options) {
|
|
|
5669
5674
|
var init_permissions = __esm({
|
|
5670
5675
|
"src/lib/permissions.ts"() {
|
|
5671
5676
|
"use strict";
|
|
5672
|
-
init_constants();
|
|
5673
5677
|
init_storage2();
|
|
5674
5678
|
init_profiles();
|
|
5675
5679
|
init_errors();
|
|
@@ -5787,7 +5791,14 @@ function createReplicationEventSink(profileRoot, options) {
|
|
|
5787
5791
|
`);
|
|
5788
5792
|
} else if (event.type === "replication.sync") {
|
|
5789
5793
|
const pending = event.pendingCleared === void 0 ? "" : ` pendingCleared:${event.pendingCleared}`;
|
|
5790
|
-
|
|
5794
|
+
const origin = event.origin === void 0 ? "" : ` origin:${event.origin}`;
|
|
5795
|
+
process.stderr.write(`[replication] sync ${event.replica} ${event.outcome}${event.code ? ` ${event.code}` : ""}${pending}${origin}
|
|
5796
|
+
`);
|
|
5797
|
+
} else if (event.type === "replication.grant") {
|
|
5798
|
+
process.stderr.write(`[replication] grant ${event.replicaId} ${event.action}${event.code ? ` ${event.code}` : ""}${event.effectiveExpiresAt ? ` effectiveExpiresAt:${event.effectiveExpiresAt}` : ""}
|
|
5799
|
+
`);
|
|
5800
|
+
} else if (event.type === "replication.lifecycle") {
|
|
5801
|
+
process.stderr.write(`[replication] lifecycle ${event.action}${event.reason ? ` ${event.reason}` : ""}${event.gate ? ` gate:${event.gate}` : ""}${event.code ? ` ${event.code}` : ""}${event.remaining === void 0 ? "" : ` remaining:${event.remaining}`}
|
|
5791
5802
|
`);
|
|
5792
5803
|
}
|
|
5793
5804
|
}
|
|
@@ -7789,7 +7800,7 @@ var init_dist = __esm({
|
|
|
7789
7800
|
// src/lib/replication-config.ts
|
|
7790
7801
|
import { join as join5 } from "path";
|
|
7791
7802
|
import { CLI_NAME as CLI_NAME8 } from "@tinycloud/operations/cli-name";
|
|
7792
|
-
import { profilePath, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
|
|
7803
|
+
import { profilePath as profilePath2, withProfileLock as withProfileLock2 } from "@tinycloud/operations/state";
|
|
7793
7804
|
import { sqliteReplicaStorage } from "@tinycloud/node-sdk";
|
|
7794
7805
|
function createCliReplicationConfig(profile, settings, output, enabled) {
|
|
7795
7806
|
if (!enabled || !settings || settings.prefixes.length === 0) return void 0;
|
|
@@ -7807,7 +7818,7 @@ function createCliReplicationConfig(profile, settings, output, enabled) {
|
|
|
7807
7818
|
);
|
|
7808
7819
|
}
|
|
7809
7820
|
}
|
|
7810
|
-
const root =
|
|
7821
|
+
const root = profilePath2(profile);
|
|
7811
7822
|
return {
|
|
7812
7823
|
enabled: true,
|
|
7813
7824
|
prefixes: settings.prefixes,
|
|
@@ -8293,17 +8304,21 @@ var init_host = __esm({
|
|
|
8293
8304
|
});
|
|
8294
8305
|
|
|
8295
8306
|
// src/config/profiles.ts
|
|
8296
|
-
import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2
|
|
8307
|
+
import { chmod as chmod2, lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
|
|
8297
8308
|
import { CLI_NAME as CLI_NAME11 } from "@tinycloud/operations/cli-name";
|
|
8298
8309
|
import { join as join6 } from "path";
|
|
8299
8310
|
import {
|
|
8300
|
-
|
|
8311
|
+
assertRealProfileDirectory,
|
|
8312
|
+
ProfileSymlinkError as ProfileSymlinkError2,
|
|
8313
|
+
namedProfilePath,
|
|
8314
|
+
profilePath as profilePath3,
|
|
8301
8315
|
readSession,
|
|
8302
8316
|
recordProfileDeletion,
|
|
8303
8317
|
refuseWriteToDeletedProfile as refuseWriteToDeletedProfile2,
|
|
8304
8318
|
removeSession,
|
|
8305
8319
|
withProfileLock as withProfileLock3,
|
|
8306
|
-
writeSession
|
|
8320
|
+
writeSession,
|
|
8321
|
+
withPinnedProfileDirectory as withPinnedProfileDirectory2
|
|
8307
8322
|
} from "@tinycloud/operations/state";
|
|
8308
8323
|
var ProfileManager;
|
|
8309
8324
|
var init_profiles = __esm({
|
|
@@ -8345,8 +8360,13 @@ var init_profiles = __esm({
|
|
|
8345
8360
|
}
|
|
8346
8361
|
/** Owner-only profile directory (0700), created or tightened before any write into it. */
|
|
8347
8362
|
static async ensureProfileDir(name) {
|
|
8363
|
+
const profileDir = profilePath3(name);
|
|
8364
|
+
if (profileDir !== join6(PROFILES_DIR, name)) {
|
|
8365
|
+
await chmod2(profileDir, PRIVATE_DIR_MODE);
|
|
8366
|
+
return profileDir;
|
|
8367
|
+
}
|
|
8348
8368
|
await _ProfileManager.ensureConfigDir();
|
|
8349
|
-
|
|
8369
|
+
await assertRealProfileDirectory(name);
|
|
8350
8370
|
await ensureDir(profileDir);
|
|
8351
8371
|
await chmod2(profileDir, PRIVATE_DIR_MODE);
|
|
8352
8372
|
return profileDir;
|
|
@@ -8375,8 +8395,7 @@ var init_profiles = __esm({
|
|
|
8375
8395
|
* Throws CLIError if the profile doesn't exist.
|
|
8376
8396
|
*/
|
|
8377
8397
|
static async getProfile(name) {
|
|
8378
|
-
const
|
|
8379
|
-
const profile = await readJson(profilePath3);
|
|
8398
|
+
const profile = await withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "profile.json")) : Promise.resolve(null));
|
|
8380
8399
|
if (!profile) {
|
|
8381
8400
|
throw new CLIError(
|
|
8382
8401
|
"PROFILE_NOT_FOUND",
|
|
@@ -8396,13 +8415,13 @@ var init_profiles = __esm({
|
|
|
8396
8415
|
}
|
|
8397
8416
|
/** Removes profile.json under the profile lock (rollback of a profile a failed login created). */
|
|
8398
8417
|
static async removeProfileConfig(name) {
|
|
8399
|
-
await _ProfileManager.withLock(name, () => rm2(join6(
|
|
8418
|
+
await _ProfileManager.withLock(name, () => rm2(join6(profilePath3(name), "profile.json"), { force: true }));
|
|
8400
8419
|
}
|
|
8401
8420
|
/**
|
|
8402
8421
|
* Returns true if a profile directory exists.
|
|
8403
8422
|
*/
|
|
8404
8423
|
static async profileExists(name) {
|
|
8405
|
-
return fileExists(join6(
|
|
8424
|
+
return withPinnedProfileDirectory2(name, (directory) => directory ? fileExists(join6(directory, "profile.json")) : Promise.resolve(false));
|
|
8406
8425
|
}
|
|
8407
8426
|
/**
|
|
8408
8427
|
* Returns an array of profile directory names.
|
|
@@ -8426,7 +8445,7 @@ var init_profiles = __esm({
|
|
|
8426
8445
|
static async deleteProfile(name) {
|
|
8427
8446
|
let profileDir;
|
|
8428
8447
|
try {
|
|
8429
|
-
profileDir =
|
|
8448
|
+
profileDir = namedProfilePath(name);
|
|
8430
8449
|
} catch {
|
|
8431
8450
|
throw new CLIError(
|
|
8432
8451
|
"INVALID_PROFILE_NAME",
|
|
@@ -8451,23 +8470,21 @@ var init_profiles = __esm({
|
|
|
8451
8470
|
return;
|
|
8452
8471
|
}
|
|
8453
8472
|
await _ProfileManager.withLock(name, async () => {
|
|
8454
|
-
|
|
8455
|
-
for (const
|
|
8456
|
-
|
|
8473
|
+
const directory = profilePath3(name);
|
|
8474
|
+
for (const file of ["session.json", "key.json"]) await rm2(join6(directory, file), { force: true });
|
|
8475
|
+
for (const entry of await readdir2(directory)) {
|
|
8476
|
+
if (entry !== ".lock" && entry !== "profile.json") await rm2(join6(directory, entry), { recursive: true, force: true });
|
|
8457
8477
|
}
|
|
8458
|
-
await rm2(join6(
|
|
8478
|
+
await rm2(join6(directory, "profile.json"), { force: true });
|
|
8459
8479
|
await recordProfileDeletion(name);
|
|
8460
8480
|
});
|
|
8461
|
-
await rmdir(profileDir).catch((error) => {
|
|
8462
|
-
if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY" && error.code !== "EEXIST") throw error;
|
|
8463
|
-
});
|
|
8464
8481
|
}
|
|
8465
8482
|
// ── Key management ──────────────────────────────────────────────────
|
|
8466
8483
|
/**
|
|
8467
8484
|
* Returns the parsed JWK for a profile, or null if no key exists.
|
|
8468
8485
|
*/
|
|
8469
8486
|
static async getKey(name) {
|
|
8470
|
-
return readJson(join6(
|
|
8487
|
+
return withPinnedProfileDirectory2(name, (directory) => directory ? readJson(join6(directory, "key.json")) : Promise.resolve(null));
|
|
8471
8488
|
}
|
|
8472
8489
|
/**
|
|
8473
8490
|
* Saves a JWK key under the profile lock (0600, in an owner-only profile
|
|
@@ -8482,7 +8499,7 @@ var init_profiles = __esm({
|
|
|
8482
8499
|
}
|
|
8483
8500
|
/** Removes key.json under the profile lock (rollback of a key a failed login created). */
|
|
8484
8501
|
static async removeKey(name) {
|
|
8485
|
-
await _ProfileManager.withLock(name, () => rm2(join6(
|
|
8502
|
+
await _ProfileManager.withLock(name, () => rm2(join6(profilePath3(name), "key.json"), { force: true }));
|
|
8486
8503
|
}
|
|
8487
8504
|
// ── Session management ──────────────────────────────────────────────
|
|
8488
8505
|
/**
|
|
@@ -8509,10 +8526,13 @@ var init_profiles = __esm({
|
|
|
8509
8526
|
* created or tightened to 0700.
|
|
8510
8527
|
*/
|
|
8511
8528
|
static async getCacheDir(name) {
|
|
8512
|
-
const
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8529
|
+
const alreadyPinned = profilePath3(name) !== namedProfilePath(name);
|
|
8530
|
+
return _ProfileManager.withLock(name, async () => {
|
|
8531
|
+
const cacheDir = join6(await _ProfileManager.ensureProfileDir(name), "cache");
|
|
8532
|
+
await ensureDir(cacheDir);
|
|
8533
|
+
await chmod2(cacheDir, PRIVATE_DIR_MODE);
|
|
8534
|
+
return alreadyPinned ? cacheDir : join6(namedProfilePath(name), "cache");
|
|
8535
|
+
});
|
|
8516
8536
|
}
|
|
8517
8537
|
// ── Resolution helpers ──────────────────────────────────────────────
|
|
8518
8538
|
/**
|
|
@@ -8536,7 +8556,8 @@ var init_profiles = __esm({
|
|
|
8536
8556
|
try {
|
|
8537
8557
|
profileConfig = await _ProfileManager.getProfile(profile);
|
|
8538
8558
|
profileHost = profileConfig.host;
|
|
8539
|
-
} catch {
|
|
8559
|
+
} catch (error) {
|
|
8560
|
+
if (error instanceof ProfileSymlinkError2) throw error;
|
|
8540
8561
|
}
|
|
8541
8562
|
const envReplication = process.env.TC_REPLICATION?.toLowerCase();
|
|
8542
8563
|
const envEnabled = envReplication === "1" || envReplication === "true" || envReplication === "on";
|