@tinycloud/cli 1.3.0-beta.52 → 1.3.0-beta.54
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 +9 -1
- package/dist/legacy-entry.js +612 -163
- package/dist/legacy-entry.js.map +1 -1
- package/package.json +1 -1
- package/skills/tc-cli/AUTH.md +3 -1
- package/skills/tc-cli/SKILL.md +19 -5
package/dist/legacy-entry.js
CHANGED
|
@@ -2734,11 +2734,11 @@ async function getLastPermissionRequestArtifact(profile) {
|
|
|
2734
2734
|
return existing.at(-1) ?? null;
|
|
2735
2735
|
}
|
|
2736
2736
|
async function replayAdditionalDelegations(node, profile, options) {
|
|
2737
|
-
const { operationSpaceResolver:
|
|
2737
|
+
const { operationSpaceResolver: operationSpaceResolver3, replayProfileDelegations } = await import("@tinycloud/operations/delegation-binding");
|
|
2738
2738
|
const { warnings } = await replayProfileDelegations(profile, node, {
|
|
2739
2739
|
host: options.host,
|
|
2740
2740
|
migrate: options.migrate,
|
|
2741
|
-
resolveSpace:
|
|
2741
|
+
resolveSpace: operationSpaceResolver3(node, options.ownerSpace)
|
|
2742
2742
|
});
|
|
2743
2743
|
return warnings;
|
|
2744
2744
|
}
|
|
@@ -11083,15 +11083,15 @@ var makeIssue = (params) => {
|
|
|
11083
11083
|
message: issueData.message
|
|
11084
11084
|
};
|
|
11085
11085
|
}
|
|
11086
|
-
let
|
|
11086
|
+
let errorMessage3 = "";
|
|
11087
11087
|
const maps = errorMaps.filter((m) => !!m).slice().reverse();
|
|
11088
11088
|
for (const map of maps) {
|
|
11089
|
-
|
|
11089
|
+
errorMessage3 = map(fullIssue, { data, defaultError: errorMessage3 }).message;
|
|
11090
11090
|
}
|
|
11091
11091
|
return {
|
|
11092
11092
|
...issueData,
|
|
11093
11093
|
path: fullPath,
|
|
11094
|
-
message:
|
|
11094
|
+
message: errorMessage3
|
|
11095
11095
|
};
|
|
11096
11096
|
};
|
|
11097
11097
|
var EMPTY_PATH = [];
|
|
@@ -16612,15 +16612,15 @@ var makeIssue2 = (params) => {
|
|
|
16612
16612
|
message: issueData.message
|
|
16613
16613
|
};
|
|
16614
16614
|
}
|
|
16615
|
-
let
|
|
16615
|
+
let errorMessage3 = "";
|
|
16616
16616
|
const maps = errorMaps.filter((m) => !!m).slice().reverse();
|
|
16617
16617
|
for (const map of maps) {
|
|
16618
|
-
|
|
16618
|
+
errorMessage3 = map(fullIssue, { data, defaultError: errorMessage3 }).message;
|
|
16619
16619
|
}
|
|
16620
16620
|
return {
|
|
16621
16621
|
...issueData,
|
|
16622
16622
|
path: fullPath,
|
|
16623
|
-
message:
|
|
16623
|
+
message: errorMessage3
|
|
16624
16624
|
};
|
|
16625
16625
|
};
|
|
16626
16626
|
var EMPTY_PATH2 = [];
|
|
@@ -23658,15 +23658,15 @@ var makeIssue3 = (params) => {
|
|
|
23658
23658
|
message: issueData.message
|
|
23659
23659
|
};
|
|
23660
23660
|
}
|
|
23661
|
-
let
|
|
23661
|
+
let errorMessage3 = "";
|
|
23662
23662
|
const maps = errorMaps.filter((m) => !!m).slice().reverse();
|
|
23663
23663
|
for (const map of maps) {
|
|
23664
|
-
|
|
23664
|
+
errorMessage3 = map(fullIssue, { data, defaultError: errorMessage3 }).message;
|
|
23665
23665
|
}
|
|
23666
23666
|
return {
|
|
23667
23667
|
...issueData,
|
|
23668
23668
|
path: fullPath,
|
|
23669
|
-
message:
|
|
23669
|
+
message: errorMessage3
|
|
23670
23670
|
};
|
|
23671
23671
|
};
|
|
23672
23672
|
var EMPTY_PATH3 = [];
|
|
@@ -29616,6 +29616,9 @@ function verifyScopedLogin(data, key, sessionDid, requested, expected = {}) {
|
|
|
29616
29616
|
if (!scopeCovers(requested, approved, signed.ownerDid)) {
|
|
29617
29617
|
throw new CLIError("OPENKEY_GRANT_BROADENED", `The signed grant contains authority beyond the requested ${expected.purpose === "grant" ? "grant" : "manifest"}. ${notStored}`, ExitCode.PERMISSION_DENIED);
|
|
29618
29618
|
}
|
|
29619
|
+
if (expected.exactApproval && !scopeCovers(approved, requested, signed.ownerDid)) {
|
|
29620
|
+
throw new CLIError("OPENKEY_APPROVAL_NARROWER", `The approval was narrower than the request. Approve all of it, or ask the agent to request less. ${notStored}`, ExitCode.PERMISSION_DENIED);
|
|
29621
|
+
}
|
|
29619
29622
|
const session = withVerifiedAuthority({ ...data, jwk: key }, { ...signed, permissions: approved });
|
|
29620
29623
|
return { session, legacyNested };
|
|
29621
29624
|
}
|
|
@@ -31156,7 +31159,7 @@ var APPS_AGENT_VARIANTS = {
|
|
|
31156
31159
|
{ service: "tinycloud.sql", actions: ["tinycloud.sql/read"], within: [""] }
|
|
31157
31160
|
],
|
|
31158
31161
|
perAppOnly: false,
|
|
31159
|
-
|
|
31162
|
+
schemaForCatalogApps: false
|
|
31160
31163
|
},
|
|
31161
31164
|
write: {
|
|
31162
31165
|
grants: [
|
|
@@ -31165,9 +31168,13 @@ var APPS_AGENT_VARIANTS = {
|
|
|
31165
31168
|
{ service: "tinycloud.sql", actions: ["tinycloud.sql/read", "tinycloud.sql/write"], within: [""] }
|
|
31166
31169
|
],
|
|
31167
31170
|
perAppOnly: true,
|
|
31168
|
-
|
|
31171
|
+
schemaForCatalogApps: true
|
|
31169
31172
|
}
|
|
31170
31173
|
};
|
|
31174
|
+
var APPS_AGENT_SCHEMA_DATABASES = {
|
|
31175
|
+
"xyz.tinycloud.sample.healthlog": "healthlog"
|
|
31176
|
+
};
|
|
31177
|
+
var SCHEMA_ACTION = "tinycloud.sql/schema";
|
|
31171
31178
|
var ACTION_WORDS = {
|
|
31172
31179
|
"tinycloud.kv/get": "read files",
|
|
31173
31180
|
"tinycloud.kv/list": "list files",
|
|
@@ -31185,12 +31192,11 @@ var ACTION_WORDS = {
|
|
|
31185
31192
|
"tinycloud.sql/*": "do anything in the database",
|
|
31186
31193
|
"tinycloud.capabilities/read": "see which permissions this key holds"
|
|
31187
31194
|
};
|
|
31188
|
-
var STRUCTURE_ACTIONS =
|
|
31189
|
-
"tinycloud.sql/
|
|
31190
|
-
"tinycloud.sql/
|
|
31191
|
-
"tinycloud.sql
|
|
31192
|
-
|
|
31193
|
-
]);
|
|
31195
|
+
var STRUCTURE_ACTIONS = {
|
|
31196
|
+
"tinycloud.sql/schema": true,
|
|
31197
|
+
"tinycloud.sql/admin": true,
|
|
31198
|
+
"tinycloud.sql/*": true
|
|
31199
|
+
};
|
|
31194
31200
|
var APP_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
31195
31201
|
var APP_ID_RULE = "an app id looks like xyz.example.app: letters, digits, dots, hyphens and underscores, no slashes or spaces";
|
|
31196
31202
|
function isAppId(value) {
|
|
@@ -31198,7 +31204,15 @@ function isAppId(value) {
|
|
|
31198
31204
|
}
|
|
31199
31205
|
function appsAgentPermissions(mode, appIds = []) {
|
|
31200
31206
|
const bases = appIds.length === 0 ? [""] : appIds.map((appId) => `${appId}/`);
|
|
31201
|
-
|
|
31207
|
+
const grants = APPS_AGENT_VARIANTS[mode].grants.flatMap(({ service, actions, within }) => within.flatMap((folder) => bases.map((base4) => ({ service, space: APPS_AGENT_SPACE, path: `${base4}${folder}`, actions: [...actions] }))));
|
|
31208
|
+
const schema = APPS_AGENT_VARIANTS[mode].schemaForCatalogApps ? schemaPathsFor(appIds).map((path) => ({ service: "tinycloud.sql", space: APPS_AGENT_SPACE, path, actions: [SCHEMA_ACTION] })) : [];
|
|
31209
|
+
return [...grants, ...schema];
|
|
31210
|
+
}
|
|
31211
|
+
function schemaPathsFor(appIds) {
|
|
31212
|
+
return appIds.flatMap((appId) => {
|
|
31213
|
+
if (!Object.hasOwn(APPS_AGENT_SCHEMA_DATABASES, appId)) return [];
|
|
31214
|
+
return [`${appId}/${APPS_AGENT_SCHEMA_DATABASES[appId]}`];
|
|
31215
|
+
});
|
|
31202
31216
|
}
|
|
31203
31217
|
function quoteApps(appIds) {
|
|
31204
31218
|
return appIds.map((appId) => `\`${appId}\``).join(", ");
|
|
@@ -31221,12 +31235,14 @@ function appsAgentScopeNotice(mode, appIds = []) {
|
|
|
31221
31235
|
const reads = writes ? `read everything there, write knowledge and data files (${writableFolders}), and add, change and delete table rows` : "list and read its files and read its table rows";
|
|
31222
31236
|
const scope = appIds.length === 0 ? `ALL your installed apps' data in the \`${APPS_AGENT_SPACE}\` space` : `the data of ${quoteApps(appIds)} (everything under ${appIds.map((appId) => `\`${appId}/\``).join(", ")}) in the \`${APPS_AGENT_SPACE}\` space`;
|
|
31223
31237
|
const lines = [
|
|
31224
|
-
|
|
31238
|
+
writes ? `This gives the agent access to ${scope}: it can ${reads}.` : `This gives the agent READ-ONLY access to ${scope}: it can ${reads}.`
|
|
31225
31239
|
];
|
|
31226
|
-
if (
|
|
31240
|
+
if (writes) {
|
|
31241
|
+
const schemaPaths = schemaPathsFor(appIds).map((path) => `\`${path}\``);
|
|
31227
31242
|
lines.push(
|
|
31228
31243
|
`It cannot change ${appIds.length === 1 ? "the app's" : "the apps'"} code, manifest or install record: files outside those folders are read-only for it.`,
|
|
31229
|
-
`SQL write is separate from that: it can change ${appIds.length === 1 ? "this app's" : "these apps'"} table rows
|
|
31244
|
+
`SQL write is separate from that: it can add, change and delete ${appIds.length === 1 ? "this app's" : "these apps'"} table rows.`,
|
|
31245
|
+
variant.schemaForCatalogApps && schemaPaths.length > 0 ? `It can also create, change and drop the tables in ${schemaPaths.join(", ")}, which the app needs to set itself up. That grant names no other database for table changes. Nodes before 1.21.6 do not enforce that database limit, so the write grant may still let it change tables elsewhere there.` : "This grant does not include permission to create, change or drop tables (tinycloud.sql/schema). Nodes before 1.21.6 do not enforce that split, so the write grant may still let it do so there.",
|
|
31230
31246
|
"It cannot touch other apps, other spaces or secrets."
|
|
31231
31247
|
);
|
|
31232
31248
|
} else {
|
|
@@ -31379,12 +31395,13 @@ function describeEntry(entry, applicationsSpace) {
|
|
|
31379
31395
|
function withinPresetSet(entry, applicationsSpace) {
|
|
31380
31396
|
if (!inApplicationsSpace(entry, applicationsSpace) || entry.caveats !== void 0) return false;
|
|
31381
31397
|
if (entry.path.split("/").some((segment) => segment === "." || segment === "..")) return false;
|
|
31398
|
+
if (entry.actions.includes(SCHEMA_ACTION)) return false;
|
|
31382
31399
|
const slash = entry.path.indexOf("/");
|
|
31383
31400
|
const folder = slash > 0 && isAppId(entry.path.slice(0, slash)) ? entry.path.slice(slash + 1) : void 0;
|
|
31384
31401
|
const grants = Object.values(APPS_AGENT_VARIANTS).flatMap(({ grants: variantGrants }) => variantGrants.filter((grant) => grant.service === entry.service));
|
|
31385
31402
|
return entry.actions.length > 0 && entry.actions.every((action) => grants.some((grant) => grant.actions.includes(action) && (!(action.endsWith("/put") || action.endsWith("/write")) || folder !== void 0 && grant.within.some((within) => folder.startsWith(within)))));
|
|
31386
31403
|
}
|
|
31387
|
-
function describeGrantScope(requested, requestedExpiry, applicationsSpace) {
|
|
31404
|
+
function describeGrantScope(requested, requestedExpiry, applicationsSpace, maxLifetimeMs) {
|
|
31388
31405
|
const preset = matchAppsAgentRequest(requested, requestedExpiry, applicationsSpace);
|
|
31389
31406
|
if (preset !== void 0) {
|
|
31390
31407
|
return { text: appsAgentScopeNotice(preset.mode, preset.appIds), entries: requested, extras: [] };
|
|
@@ -31397,11 +31414,11 @@ function describeGrantScope(requested, requestedExpiry, applicationsSpace) {
|
|
|
31397
31414
|
...extras.map((entry) => `! ${describeEntry(entry, applicationsSpace)}`),
|
|
31398
31415
|
...others.map((entry) => `- ${describeEntry(entry, applicationsSpace)}`)
|
|
31399
31416
|
];
|
|
31400
|
-
if (requested.some((entry) => entry.service === "tinycloud.sql" && entry.actions.some((action) =>
|
|
31401
|
-
lines.push("
|
|
31417
|
+
if (requested.some((entry) => entry.service === "tinycloud.sql" && entry.actions.some((action) => Object.hasOwn(STRUCTURE_ACTIONS, action)))) {
|
|
31418
|
+
lines.push("This request can create, change and drop tables (an SQL schema, admin or wildcard entry).");
|
|
31402
31419
|
}
|
|
31403
31420
|
const entryLifetimes = requested.filter((entry) => entry.expiry !== void 0).map((entry) => lifetimeMs(entry.expiry));
|
|
31404
|
-
const durations = [lifetimeMs(requestedExpiry), ...entryLifetimes].filter((ms2) => ms2 !== void 0);
|
|
31421
|
+
const durations = [lifetimeMs(requestedExpiry), ...entryLifetimes].filter((ms2) => ms2 !== void 0).map((ms2) => Math.min(ms2, maxLifetimeMs ?? Number.POSITIVE_INFINITY));
|
|
31405
31422
|
const longest = Math.max(...durations);
|
|
31406
31423
|
lines.push(
|
|
31407
31424
|
durations.length === 0 || !Number.isFinite(longest) ? "Its lifetime could not be read from the request." : `It lasts up to ${lifetimePhrase(longest)}${requestedExpiry === void 0 ? " (no lifetime was requested; this is the default)" : ""}; revoke it any time with \`${CLI_NAME16} delegation revoke <id>\`.`
|
|
@@ -32181,7 +32198,7 @@ ${signInAgainHint(ctx.profile, posture ?? void 0)}
|
|
|
32181
32198
|
`With --preset ${APPS_AGENT_PRESET}: limit the grant to this app's <app_id>/ prefix (recommended); repeat for up to ${APPS_AGENT_MAX_APPS} apps`,
|
|
32182
32199
|
(value, previous) => [...previous, value],
|
|
32183
32200
|
[]
|
|
32184
|
-
).option("--write", `With --preset ${APPS_AGENT_PRESET}: also let the agent add and change the named --app apps' data: write their knowledge/ and prefs/ files (kv put) and change their
|
|
32201
|
+
).option("--write", `With --preset ${APPS_AGENT_PRESET}: also let the agent add and change the named --app apps' data: write their knowledge/ and prefs/ files (kv put) and add, change or delete their table rows (sql write). Their code, manifest and install record stay read-only. Creating, changing or dropping tables is granted only for an app's catalog database (today Health Log's healthlog)`).option("--paste", "With --grant on an OpenKey profile: wait up to 5 minutes for a signed code from OpenKey on stdin instead of a browser callback (use when the browser cannot reach this CLI)").action(async (options2, cmd) => {
|
|
32185
32202
|
try {
|
|
32186
32203
|
const globalOpts = cmd.optsWithGlobals();
|
|
32187
32204
|
const ctx = await ProfileManager.resolveContext(globalOpts, options2.grant ? {} : { storedSession: "unused" });
|
|
@@ -32511,7 +32528,12 @@ ${appsAgentScopeNotice(preset.mode, preset.appIds)}
|
|
|
32511
32528
|
[{ service: "tinycloud.kv", space: APPS_AGENT_SPACE, path: "", actions: ["tinycloud.kv/get"] }],
|
|
32512
32529
|
ctx.profile
|
|
32513
32530
|
);
|
|
32514
|
-
const scope = describeGrantScope(
|
|
32531
|
+
const scope = describeGrantScope(
|
|
32532
|
+
requested,
|
|
32533
|
+
parsed.requestedExpiry,
|
|
32534
|
+
applications.space ?? APPS_AGENT_SPACE,
|
|
32535
|
+
profile.authMethod === "openkey" ? OPENKEY_AGENT_MAX_LIFETIME_MS : void 0
|
|
32536
|
+
);
|
|
32515
32537
|
process.stderr.write(`
|
|
32516
32538
|
${scope.text}
|
|
32517
32539
|
|
|
@@ -32530,16 +32552,43 @@ ${scope.text}
|
|
|
32530
32552
|
}
|
|
32531
32553
|
const resolvedRequest = { ...parsed, requested };
|
|
32532
32554
|
const node = await ensureAuthenticated(ctx);
|
|
32533
|
-
|
|
32534
|
-
|
|
32535
|
-
|
|
32536
|
-
|
|
32537
|
-
|
|
32538
|
-
|
|
32539
|
-
|
|
32540
|
-
|
|
32541
|
-
|
|
32542
|
-
|
|
32555
|
+
let grant;
|
|
32556
|
+
if (profile.authMethod === "openkey") {
|
|
32557
|
+
assertOpenKeyAgentRequest(requested);
|
|
32558
|
+
const approval = await acquireOpenKeyApproval({
|
|
32559
|
+
ctx,
|
|
32560
|
+
profile,
|
|
32561
|
+
requested,
|
|
32562
|
+
expiryOption: Math.min(
|
|
32563
|
+
parseRequestedExpiry(parsed.requestedExpiry ?? APPS_AGENT_EXPIRY).durationMs,
|
|
32564
|
+
OPENKEY_AGENT_MAX_LIFETIME_MS
|
|
32565
|
+
),
|
|
32566
|
+
reason: "Grant permissions requested by a TinyCloud auth request artifact.",
|
|
32567
|
+
exactApproval: true,
|
|
32568
|
+
noPopup: !(process.stdin.isTTY || process.stderr.isTTY)
|
|
32569
|
+
});
|
|
32570
|
+
const approvalNode = await nodeFromOpenKeyApproval(ctx.host, approval.session);
|
|
32571
|
+
grant = {
|
|
32572
|
+
...await grantAuthRequest(approvalNode, resolvedRequest, {
|
|
32573
|
+
expiry: approval.delegation.expiry.getTime() - Date.now()
|
|
32574
|
+
}),
|
|
32575
|
+
// The owner approved this grant on the OpenKey approval screen, so it was prompted.
|
|
32576
|
+
prompted: true
|
|
32577
|
+
};
|
|
32578
|
+
process.stderr.write(`Grant lasts until ${grant.expiry}, the lifetime of the OpenKey approval.
|
|
32579
|
+
`);
|
|
32580
|
+
} else {
|
|
32581
|
+
await ensureDelegationAuthority({
|
|
32582
|
+
ctx,
|
|
32583
|
+
profile,
|
|
32584
|
+
node,
|
|
32585
|
+
requested,
|
|
32586
|
+
expiryOption: parsed.requestedExpiry,
|
|
32587
|
+
reason: "Grant permissions requested by a TinyCloud auth request artifact.",
|
|
32588
|
+
yes: options2.yes === true || confirmedExtras
|
|
32589
|
+
});
|
|
32590
|
+
grant = await grantAuthRequest(node, resolvedRequest);
|
|
32591
|
+
}
|
|
32543
32592
|
await appendLocalGrantArtifact(ctx.profile, grant.delegation);
|
|
32544
32593
|
await appendGrantHistory(ctx.profile, {
|
|
32545
32594
|
addedCaps: grant.permissions,
|
|
@@ -32919,12 +32968,72 @@ async function activateAndStoreOpenKeyGrants(profileName2, host, node, grants, s
|
|
|
32919
32968
|
await appendAdditionalDelegations(profileName2, records);
|
|
32920
32969
|
});
|
|
32921
32970
|
}
|
|
32971
|
+
async function acquireOpenKeyApproval(params) {
|
|
32972
|
+
if (params.requested.some(isRawCidRevocationPermission)) {
|
|
32973
|
+
throw new CLIError(
|
|
32974
|
+
"RAW_RECAP_RESOURCE_UNSUPPORTED",
|
|
32975
|
+
"OpenKey cannot sign raw urn:cid revocation authority.",
|
|
32976
|
+
ExitCode.PERMISSION_DENIED
|
|
32977
|
+
);
|
|
32978
|
+
}
|
|
32979
|
+
const key = await ProfileManager.getKey(params.ctx.profile);
|
|
32980
|
+
if (!key) {
|
|
32981
|
+
throw new CLIError(
|
|
32982
|
+
"NO_KEY",
|
|
32983
|
+
`No key found for profile "${params.ctx.profile}". Run \`${CLI_NAME17} --profile ${params.ctx.profile} auth rotate\` to create a new key and sign in.`,
|
|
32984
|
+
ExitCode.AUTH_REQUIRED
|
|
32985
|
+
);
|
|
32986
|
+
}
|
|
32987
|
+
const expiryCap = params.expiryOption === void 0 ? void 0 : parseRequestedExpiry(params.expiryOption);
|
|
32988
|
+
const anchorSpace = params.anchorSpace ?? params.profile.spaceId ?? params.profile.spaceName;
|
|
32989
|
+
const request = grantRequestPermissions(params.requested, anchorSpace);
|
|
32990
|
+
const acquireOpenKey = params.openKeyAcquisition ?? startAuthFlow;
|
|
32991
|
+
const delegationData = await acquireOpenKey(params.profile.did, {
|
|
32992
|
+
jwk: key,
|
|
32993
|
+
host: params.ctx.host,
|
|
32994
|
+
permissions: request,
|
|
32995
|
+
reason: permissionGrantReason(params.reason, request),
|
|
32996
|
+
openkeyHost: resolveOpenKeyHost(params.profile),
|
|
32997
|
+
expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap),
|
|
32998
|
+
noPopup: params.noPopup
|
|
32999
|
+
});
|
|
33000
|
+
return verifyOpenKeyApproval(delegationData, request, params.ctx.host, {
|
|
33001
|
+
key,
|
|
33002
|
+
sessionDid: params.profile.sessionDid ?? params.profile.did,
|
|
33003
|
+
expectedOwner: pinnedOwner(params.profile),
|
|
33004
|
+
expiry: expiryCap,
|
|
33005
|
+
exactApproval: params.exactApproval === true
|
|
33006
|
+
});
|
|
33007
|
+
}
|
|
33008
|
+
async function nodeFromOpenKeyApproval(host, session) {
|
|
33009
|
+
const ownerParts = session.ownerDid.split(":");
|
|
33010
|
+
const node = new TinyCloudNode3({ host });
|
|
33011
|
+
await node.restoreSession({
|
|
33012
|
+
delegationHeader: session.delegationHeader,
|
|
33013
|
+
delegationCid: session.delegationCid,
|
|
33014
|
+
spaceId: session.spaceId,
|
|
33015
|
+
jwk: session.jwk,
|
|
33016
|
+
verificationMethod: session.verificationMethod,
|
|
33017
|
+
address: ownerParts[4],
|
|
33018
|
+
chainId: Number(ownerParts[3]),
|
|
33019
|
+
siwe: session.siwe,
|
|
33020
|
+
signature: session.signature,
|
|
33021
|
+
expiresAt: session.expiresAt,
|
|
33022
|
+
tinycloudHosts: [host]
|
|
33023
|
+
});
|
|
33024
|
+
return node;
|
|
33025
|
+
}
|
|
32922
33026
|
async function ensureDelegationAuthority(params) {
|
|
32923
33027
|
if (!params.force && params.node.hasRuntimePermissions(params.requested)) return void 0;
|
|
32924
33028
|
if (!params.consentAlreadyGiven && (params.persist === false || params.profile.authMethod !== "openkey")) {
|
|
32925
33029
|
await requirePermissionConsent(params.requested, params.yes);
|
|
32926
33030
|
}
|
|
32927
33031
|
if (params.profile.authMethod === "openkey") {
|
|
33032
|
+
if (params.persist === false) {
|
|
33033
|
+
const { delegation } = await acquireOpenKeyApproval(params);
|
|
33034
|
+
await params.node.useRuntimeDelegation(delegation);
|
|
33035
|
+
return delegation;
|
|
33036
|
+
}
|
|
32928
33037
|
if (params.requested.some(isRawCidRevocationPermission)) {
|
|
32929
33038
|
throw new CLIError(
|
|
32930
33039
|
"RAW_RECAP_RESOURCE_UNSUPPORTED",
|
|
@@ -32947,28 +33056,10 @@ async function ensureDelegationAuthority(params) {
|
|
|
32947
33056
|
key,
|
|
32948
33057
|
sessionDid: params.profile.sessionDid ?? params.profile.did,
|
|
32949
33058
|
expectedOwner: pinnedOwner(params.profile),
|
|
32950
|
-
expiry: expiryCap
|
|
33059
|
+
expiry: expiryCap,
|
|
33060
|
+
exactApproval: params.exactApproval === true
|
|
32951
33061
|
};
|
|
32952
33062
|
const anchorSpace = params.anchorSpace ?? params.profile.spaceId ?? params.profile.spaceName;
|
|
32953
|
-
if (params.persist === false) {
|
|
32954
|
-
const request = grantRequestPermissions(params.requested, anchorSpace);
|
|
32955
|
-
const delegationData = await acquireOpenKey(params.profile.did, {
|
|
32956
|
-
jwk: key,
|
|
32957
|
-
host: params.ctx.host,
|
|
32958
|
-
permissions: request,
|
|
32959
|
-
reason: permissionGrantReason(params.reason, request),
|
|
32960
|
-
openkeyHost,
|
|
32961
|
-
expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap)
|
|
32962
|
-
});
|
|
32963
|
-
const delegation = portableFromOpenKeyDelegation(
|
|
32964
|
-
delegationData,
|
|
32965
|
-
request,
|
|
32966
|
-
params.ctx.host,
|
|
32967
|
-
proof
|
|
32968
|
-
);
|
|
32969
|
-
await params.node.useRuntimeDelegation(delegation);
|
|
32970
|
-
return delegation;
|
|
32971
|
-
}
|
|
32972
33063
|
const grants = [];
|
|
32973
33064
|
for (const group of groupPermissionsBySpace(params.requested)) {
|
|
32974
33065
|
const request = grantRequestPermissions(group, anchorSpace);
|
|
@@ -32978,7 +33069,8 @@ async function ensureDelegationAuthority(params) {
|
|
|
32978
33069
|
permissions: request,
|
|
32979
33070
|
reason: permissionGrantReason(params.reason, group),
|
|
32980
33071
|
openkeyHost,
|
|
32981
|
-
expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap)
|
|
33072
|
+
expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap),
|
|
33073
|
+
noPopup: params.noPopup
|
|
32982
33074
|
});
|
|
32983
33075
|
const delegation = portableFromOpenKeyDelegation(delegationData, request, params.ctx.host, proof);
|
|
32984
33076
|
grants.push({ delegation, effective: permissionsFromDelegation(delegation) });
|
|
@@ -33005,6 +33097,23 @@ async function ensureDelegationAuthority(params) {
|
|
|
33005
33097
|
});
|
|
33006
33098
|
}
|
|
33007
33099
|
}
|
|
33100
|
+
var OPENKEY_AGENT_MAX_LIFETIME_MS = parseRequestedExpiry(APPS_AGENT_EXPIRY).durationMs;
|
|
33101
|
+
function assertOpenKeyAgentRequest(requested) {
|
|
33102
|
+
if (requested.some((permission) => isRawEncryptionPermission(permission) || isRawCidRevocationPermission(permission))) {
|
|
33103
|
+
throw new CLIError(
|
|
33104
|
+
"RAW_RECAP_RESOURCE_UNSUPPORTED",
|
|
33105
|
+
"OpenKey cannot approve raw urn:cid or raw encryption authority for an agent request.",
|
|
33106
|
+
ExitCode.PERMISSION_DENIED
|
|
33107
|
+
);
|
|
33108
|
+
}
|
|
33109
|
+
if (groupPermissionsBySpace(requested).length !== 1) {
|
|
33110
|
+
throw new CLIError(
|
|
33111
|
+
"AGENT_GRANT_MULTIPLE_SPACES",
|
|
33112
|
+
"An agent grant must cover exactly one space. Request one space per auth request.",
|
|
33113
|
+
ExitCode.PERMISSION_DENIED
|
|
33114
|
+
);
|
|
33115
|
+
}
|
|
33116
|
+
}
|
|
33008
33117
|
function permissionGrantReason(context, permissions) {
|
|
33009
33118
|
const first = permissions[0];
|
|
33010
33119
|
const summary = first ? compactPermission(first) : "no permissions";
|
|
@@ -33127,10 +33236,14 @@ function groupPermissionsBySpace(permissions) {
|
|
|
33127
33236
|
return grouped;
|
|
33128
33237
|
}
|
|
33129
33238
|
function portableFromOpenKeyDelegation(data, requested, host, proof) {
|
|
33239
|
+
return verifyOpenKeyApproval(data, requested, host, proof).delegation;
|
|
33240
|
+
}
|
|
33241
|
+
function verifyOpenKeyApproval(data, requested, host, proof) {
|
|
33130
33242
|
const { session, legacyNested } = verifyScopedLogin(data, proof.key, proof.sessionDid, requested, {
|
|
33131
33243
|
expectedOwner: proof.expectedOwner,
|
|
33132
33244
|
expiry: proof.expiry,
|
|
33133
|
-
purpose: "grant"
|
|
33245
|
+
purpose: "grant",
|
|
33246
|
+
exactApproval: proof.exactApproval
|
|
33134
33247
|
});
|
|
33135
33248
|
if (legacyNested.length > 0) {
|
|
33136
33249
|
throw new CLIError(
|
|
@@ -33153,7 +33266,7 @@ function portableFromOpenKeyDelegation(data, requested, host, proof) {
|
|
|
33153
33266
|
...permission.caveats === void 0 ? {} : { caveats: structuredClone(permission.caveats) }
|
|
33154
33267
|
}));
|
|
33155
33268
|
const ownerParts = session.ownerDid.split(":");
|
|
33156
|
-
|
|
33269
|
+
const delegation = {
|
|
33157
33270
|
cid: data.delegationCid,
|
|
33158
33271
|
delegationHeader: data.delegationHeader,
|
|
33159
33272
|
spaceId: returnedSpace,
|
|
@@ -33168,6 +33281,7 @@ function portableFromOpenKeyDelegation(data, requested, host, proof) {
|
|
|
33168
33281
|
// Verified above; replay rebuilds the CACAO from it before trusting the grant.
|
|
33169
33282
|
siweProof: { siwe: data.siwe, signature: data.signature }
|
|
33170
33283
|
};
|
|
33284
|
+
return { delegation, session };
|
|
33171
33285
|
}
|
|
33172
33286
|
async function rotateAuthKey(profileName2, host, options = {}) {
|
|
33173
33287
|
await recoverPendingLogout(profileName2);
|
|
@@ -34312,12 +34426,369 @@ function registerEnableCommand(program) {
|
|
|
34312
34426
|
});
|
|
34313
34427
|
}
|
|
34314
34428
|
|
|
34429
|
+
// src/commands/apps.ts
|
|
34430
|
+
init_profiles();
|
|
34431
|
+
init_constants();
|
|
34432
|
+
init_sdk();
|
|
34433
|
+
import { CLI_NAME as CLI_NAME23 } from "@tinycloud/operations/cli-name";
|
|
34434
|
+
import { operationSpaceResolver as operationSpaceResolver2 } from "@tinycloud/operations/delegation-binding";
|
|
34435
|
+
|
|
34436
|
+
// src/lib/knowledge-input.ts
|
|
34437
|
+
init_constants();
|
|
34438
|
+
init_errors();
|
|
34439
|
+
init_safe_text();
|
|
34440
|
+
function usage2(message) {
|
|
34441
|
+
return new CLIError("INVALID_ARGUMENT", message, ExitCode.USAGE_ERROR);
|
|
34442
|
+
}
|
|
34443
|
+
function assertKnowledgeAppId(value) {
|
|
34444
|
+
if (!isAppId(value)) throw usage2(`Invalid app id "${safeClipped(value)}": ${APP_ID_RULE}.`);
|
|
34445
|
+
return value;
|
|
34446
|
+
}
|
|
34447
|
+
function assertKnowledgeConcept(value) {
|
|
34448
|
+
const reason = value.length > 300 ? "it is longer than 300 characters" : value.startsWith("/") ? "it starts with a slash" : /[*\\]/.test(value) ? "it contains a wildcard or backslash" : /%(?:2e|2f|5c)/i.test(value) ? "it contains an encoded dot, slash or backslash" : value.split("/").some((segment) => segment === "" || segment === "." || segment === "..") ? "it has an empty, `.` or `..` segment" : void 0;
|
|
34449
|
+
if (reason !== void 0) {
|
|
34450
|
+
throw usage2(
|
|
34451
|
+
`Invalid concept "${safeClipped(value)}": ${reason}. Use a path inside the app's knowledge folder, such as app or tables/meal.md.`
|
|
34452
|
+
);
|
|
34453
|
+
}
|
|
34454
|
+
return value;
|
|
34455
|
+
}
|
|
34456
|
+
|
|
34457
|
+
// src/lib/apps-install-pin.ts
|
|
34458
|
+
var SHARE_ORIGIN = "https://share.tinycloud.xyz";
|
|
34459
|
+
var HEALTHLOG_ID = "xyz.tinycloud.sample.healthlog";
|
|
34460
|
+
var PINNED = {
|
|
34461
|
+
[HEALTHLOG_ID]: {
|
|
34462
|
+
appId: HEALTHLOG_ID,
|
|
34463
|
+
database: `${HEALTHLOG_ID}/healthlog`,
|
|
34464
|
+
bundleDigest: "dc3ec19fffce8fee350944469948531659a258347538a98aacdd5544deea5014",
|
|
34465
|
+
files: [
|
|
34466
|
+
{ path: "schema.sql", sha256: "bb46ab35ac7887f672beba2f50b9f982c31d41607b3fa77ac9c2e72003edf199", size: 4105 },
|
|
34467
|
+
{ path: "knowledge/app.md", sha256: "74a65d0332ba06fe5f7fb47c67243ebc6ca80435f3388cb5201712287240f4c7", size: 3994 },
|
|
34468
|
+
{ path: "knowledge/kv/app.md", sha256: "1583fdefdf6261a8ce110d8de22843855f8393f760de9fa79c3491868924bd42", size: 1283 },
|
|
34469
|
+
{ path: "knowledge/kv/knowledge.md", sha256: "1f3fb9d50e0ccea10dbe4a223efdbbde2b277bbed8f4790251331c5a7dcebc0f", size: 1170 },
|
|
34470
|
+
{ path: "knowledge/kv/prefs.md", sha256: "0de88eb2cd8c2ad7302b84c2f6b972364d9e9354f15b70657349339167dd52e4", size: 831 },
|
|
34471
|
+
{ path: "knowledge/log.md", sha256: "7753435d03afe51c36023c596330c7f04c597319586b5b39d4cf8a675512b1d0", size: 263 },
|
|
34472
|
+
{ path: "knowledge/tables/meal.md", sha256: "1bc09271c8c2cf987ef81dfb99c4427234fdfbc795f5229a1d631aef0fbff795", size: 1921 },
|
|
34473
|
+
{ path: "knowledge/tables/weight.md", sha256: "fbae8b103931f976b838024d3ec7ea2596f1b1af3e9834e8aaba1d216d227bc0", size: 1401 },
|
|
34474
|
+
{ path: "knowledge/views/day_card.md", sha256: "f1ea0738b6905a4952ab1f9c269310f2f888c6606b16304c12bdf69dfb3fafbe", size: 1707 },
|
|
34475
|
+
{ path: "knowledge/views/week_rollup.md", sha256: "b9773f2ae10fadb9b0ce5f5a78885c87f90bd202a00f7f157428911239c2077e", size: 1579 },
|
|
34476
|
+
{ path: "knowledge/index.md", sha256: "06e54fa6a4f14ae13bdbcf2ad5ab22b6d561da90e04c27fc616787762e45811e", size: 945 }
|
|
34477
|
+
]
|
|
34478
|
+
}
|
|
34479
|
+
};
|
|
34480
|
+
function pinnedApp(appId) {
|
|
34481
|
+
return Object.hasOwn(PINNED, appId) ? PINNED[appId] : void 0;
|
|
34482
|
+
}
|
|
34483
|
+
|
|
34484
|
+
// src/lib/apps-install.ts
|
|
34485
|
+
import { createHash as createHash3 } from "crypto";
|
|
34486
|
+
function errorMessage2(error) {
|
|
34487
|
+
return error instanceof Error ? error.message : String(error);
|
|
34488
|
+
}
|
|
34489
|
+
var MAX_FILE_BYTES = 1e6;
|
|
34490
|
+
var AppsInstallError = class extends Error {
|
|
34491
|
+
constructor(code4, message) {
|
|
34492
|
+
super(message);
|
|
34493
|
+
this.code = code4;
|
|
34494
|
+
this.name = "AppsInstallError";
|
|
34495
|
+
}
|
|
34496
|
+
};
|
|
34497
|
+
function sha256Hex2(bytes) {
|
|
34498
|
+
return createHash3("sha256").update(bytes).digest("hex");
|
|
34499
|
+
}
|
|
34500
|
+
function bytesEqual(a, b) {
|
|
34501
|
+
return a.length === b.length && Buffer.from(a).equals(Buffer.from(b));
|
|
34502
|
+
}
|
|
34503
|
+
async function fetchVerified(app, io) {
|
|
34504
|
+
const verified = /* @__PURE__ */ new Map();
|
|
34505
|
+
for (const file of app.files) {
|
|
34506
|
+
const url = `${SHARE_ORIGIN}/apps/b/${app.bundleDigest}/${file.path}`;
|
|
34507
|
+
let bytes;
|
|
34508
|
+
try {
|
|
34509
|
+
bytes = await io.fetchBytes(url);
|
|
34510
|
+
} catch (error) {
|
|
34511
|
+
throw new AppsInstallError("FETCH_FAILED", `Could not fetch ${file.path}: ${errorMessage2(error)}`);
|
|
34512
|
+
}
|
|
34513
|
+
verifyBytes(file, bytes);
|
|
34514
|
+
verified.set(file.path, bytes);
|
|
34515
|
+
}
|
|
34516
|
+
return verified;
|
|
34517
|
+
}
|
|
34518
|
+
function verifyBytes(file, bytes) {
|
|
34519
|
+
if (bytes.length > MAX_FILE_BYTES) {
|
|
34520
|
+
throw new AppsInstallError("INTEGRITY_FAILED", `${file.path} is ${bytes.length} bytes, above the ${MAX_FILE_BYTES} byte limit`);
|
|
34521
|
+
}
|
|
34522
|
+
if (bytes.length !== file.size || sha256Hex2(bytes) !== file.sha256) {
|
|
34523
|
+
throw new AppsInstallError("INTEGRITY_FAILED", `${file.path} does not match the pinned sha256 and size`);
|
|
34524
|
+
}
|
|
34525
|
+
}
|
|
34526
|
+
function splitStatements(sql) {
|
|
34527
|
+
let stripped = "";
|
|
34528
|
+
let inQuote = false;
|
|
34529
|
+
for (let i = 0; i < sql.length; i++) {
|
|
34530
|
+
const ch = sql[i];
|
|
34531
|
+
if (ch === "'") inQuote = !inQuote;
|
|
34532
|
+
if (!inQuote && ch === "-" && sql[i + 1] === "-") {
|
|
34533
|
+
while (i < sql.length && sql[i] !== "\n") i++;
|
|
34534
|
+
stripped += "\n";
|
|
34535
|
+
continue;
|
|
34536
|
+
}
|
|
34537
|
+
stripped += ch;
|
|
34538
|
+
}
|
|
34539
|
+
return stripped.split(";").map((statement) => statement.replace(/\s+/g, " ").trim()).filter((statement) => statement.length > 0);
|
|
34540
|
+
}
|
|
34541
|
+
var TABLE_FORM = /^CREATE TABLE IF NOT EXISTS ([A-Za-z_][A-Za-z0-9_]*) \(.*\)$/i;
|
|
34542
|
+
var INDEX_FORM = /^CREATE INDEX IF NOT EXISTS ([A-Za-z_][A-Za-z0-9_]*) ON [A-Za-z_][A-Za-z0-9_]*\([A-Za-z_][A-Za-z0-9_, ]*\)$/i;
|
|
34543
|
+
var VIEW_FORM = /^CREATE VIEW IF NOT EXISTS ([A-Za-z_][A-Za-z0-9_]*) AS SELECT .+$/i;
|
|
34544
|
+
function planDdl(schemaSql) {
|
|
34545
|
+
const tables = [];
|
|
34546
|
+
const views = [];
|
|
34547
|
+
const statements = [];
|
|
34548
|
+
for (const statement of splitStatements(schemaSql)) {
|
|
34549
|
+
const table = TABLE_FORM.exec(statement);
|
|
34550
|
+
const index = INDEX_FORM.exec(statement);
|
|
34551
|
+
const view = VIEW_FORM.exec(statement);
|
|
34552
|
+
if (table) {
|
|
34553
|
+
tables.push(table[1]);
|
|
34554
|
+
statements.push(statement);
|
|
34555
|
+
} else if (index) {
|
|
34556
|
+
statements.push(statement);
|
|
34557
|
+
} else if (view) {
|
|
34558
|
+
views.push(view[1]);
|
|
34559
|
+
statements.push(`DROP VIEW IF EXISTS ${view[1]}`, statement);
|
|
34560
|
+
} else {
|
|
34561
|
+
throw new AppsInstallError("DDL_REFUSED", `Pinned schema statement is not an allowed form: ${statement.slice(0, 80)}`);
|
|
34562
|
+
}
|
|
34563
|
+
}
|
|
34564
|
+
return { tables, views, statements };
|
|
34565
|
+
}
|
|
34566
|
+
async function applySchema(plan, io) {
|
|
34567
|
+
let existing;
|
|
34568
|
+
try {
|
|
34569
|
+
existing = new Set(await io.sqlTableNames());
|
|
34570
|
+
} catch (error) {
|
|
34571
|
+
throw new AppsInstallError("SQL_FAILED", `Could not list tables: ${errorMessage2(error)}`);
|
|
34572
|
+
}
|
|
34573
|
+
for (const statement of plan.statements) {
|
|
34574
|
+
try {
|
|
34575
|
+
await io.sqlExecute(statement);
|
|
34576
|
+
} catch (error) {
|
|
34577
|
+
throw new AppsInstallError("SQL_FAILED", `Schema statement failed: ${errorMessage2(error)}`);
|
|
34578
|
+
}
|
|
34579
|
+
}
|
|
34580
|
+
return {
|
|
34581
|
+
tablesCreated: plan.tables.filter((name) => !existing.has(name)),
|
|
34582
|
+
viewsRecreated: [...plan.views]
|
|
34583
|
+
};
|
|
34584
|
+
}
|
|
34585
|
+
function knowledgePaths(app) {
|
|
34586
|
+
const paths = app.files.map((file) => file.path).filter((path) => path.startsWith("knowledge/"));
|
|
34587
|
+
return [...paths.filter((path) => path !== "knowledge/index.md"), ...paths.filter((path) => path === "knowledge/index.md")];
|
|
34588
|
+
}
|
|
34589
|
+
async function inspectKnowledge(appId, app, verified, io) {
|
|
34590
|
+
const missing = [];
|
|
34591
|
+
const unchanged = [];
|
|
34592
|
+
const conflicts = [];
|
|
34593
|
+
for (const path of knowledgePaths(app)) {
|
|
34594
|
+
const current = await readKv(`${appId}/${path}`, io);
|
|
34595
|
+
if (current === void 0) missing.push(path);
|
|
34596
|
+
else if (bytesEqual(current, verified.get(path))) unchanged.push(path);
|
|
34597
|
+
else conflicts.push(path);
|
|
34598
|
+
}
|
|
34599
|
+
return { missing, unchanged, conflicts };
|
|
34600
|
+
}
|
|
34601
|
+
async function createMissing(appId, missing, verified, io) {
|
|
34602
|
+
const written = [];
|
|
34603
|
+
const unchanged = [];
|
|
34604
|
+
const conflicts = [];
|
|
34605
|
+
for (const path of missing) {
|
|
34606
|
+
const key = `${appId}/${path}`;
|
|
34607
|
+
const bytes = verified.get(path);
|
|
34608
|
+
const outcome = await createKv(key, bytes, io);
|
|
34609
|
+
if (outcome === "created") {
|
|
34610
|
+
written.push(path);
|
|
34611
|
+
continue;
|
|
34612
|
+
}
|
|
34613
|
+
const raced = await readKv(key, io);
|
|
34614
|
+
if (raced !== void 0 && bytesEqual(raced, bytes)) unchanged.push(path);
|
|
34615
|
+
else conflicts.push(path);
|
|
34616
|
+
}
|
|
34617
|
+
return { written, unchanged, conflicts };
|
|
34618
|
+
}
|
|
34619
|
+
async function readKv(key, io) {
|
|
34620
|
+
try {
|
|
34621
|
+
return await io.kvGet(key);
|
|
34622
|
+
} catch (error) {
|
|
34623
|
+
throw new AppsInstallError("KV_FAILED", `Could not read ${key}: ${errorMessage2(error)}`);
|
|
34624
|
+
}
|
|
34625
|
+
}
|
|
34626
|
+
async function checkInstallRecord(appId, app, io) {
|
|
34627
|
+
const key = `${appId}/app/install.json`;
|
|
34628
|
+
const bytes = await readKv(key, io);
|
|
34629
|
+
if (bytes === void 0) return;
|
|
34630
|
+
const digest2 = installRecordDigest(bytes);
|
|
34631
|
+
if (digest2 === void 0) {
|
|
34632
|
+
throw new AppsInstallError("INSTALL_RECORD_INVALID", `${key} is not an install record with a digest`);
|
|
34633
|
+
}
|
|
34634
|
+
if (digest2 !== app.bundleDigest) {
|
|
34635
|
+
throw new AppsInstallError(
|
|
34636
|
+
"INSTALL_RECORD_MISMATCH",
|
|
34637
|
+
`${key} names bundle ${digest2}, but this CLI pins ${app.bundleDigest}; update the CLI before installing`
|
|
34638
|
+
);
|
|
34639
|
+
}
|
|
34640
|
+
}
|
|
34641
|
+
function installRecordDigest(bytes) {
|
|
34642
|
+
let parsed;
|
|
34643
|
+
try {
|
|
34644
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
34645
|
+
} catch {
|
|
34646
|
+
return void 0;
|
|
34647
|
+
}
|
|
34648
|
+
if (typeof parsed !== "object" || parsed === null || !("digest" in parsed)) return void 0;
|
|
34649
|
+
return typeof parsed.digest === "string" ? parsed.digest : void 0;
|
|
34650
|
+
}
|
|
34651
|
+
async function createKv(key, bytes, io) {
|
|
34652
|
+
try {
|
|
34653
|
+
return await io.kvCreate(key, bytes);
|
|
34654
|
+
} catch (error) {
|
|
34655
|
+
throw new AppsInstallError("KV_FAILED", `Could not write ${key}: ${errorMessage2(error)}`);
|
|
34656
|
+
}
|
|
34657
|
+
}
|
|
34658
|
+
async function installApp(appId, io, lookup = pinnedApp) {
|
|
34659
|
+
const app = lookup(appId);
|
|
34660
|
+
if (!app) throw new AppsInstallError("UNKNOWN_APP", `No pinned catalog entry for ${appId}`);
|
|
34661
|
+
const verified = await fetchVerified(app, io);
|
|
34662
|
+
await checkInstallRecord(appId, app, io);
|
|
34663
|
+
const schemaSql = new TextDecoder("utf-8", { fatal: true }).decode(verified.get("schema.sql"));
|
|
34664
|
+
const plan = planDdl(schemaSql);
|
|
34665
|
+
const inspected = await inspectKnowledge(appId, app, verified, io);
|
|
34666
|
+
const created = await createMissing(appId, inspected.missing, verified, io);
|
|
34667
|
+
let schema;
|
|
34668
|
+
try {
|
|
34669
|
+
schema = await applySchema(plan, io);
|
|
34670
|
+
} catch (error) {
|
|
34671
|
+
if (error instanceof AppsInstallError) {
|
|
34672
|
+
throw new AppsInstallError(error.code, `notes saved; tables not set up: run again. ${error.message}`);
|
|
34673
|
+
}
|
|
34674
|
+
throw error;
|
|
34675
|
+
}
|
|
34676
|
+
return {
|
|
34677
|
+
appId,
|
|
34678
|
+
status: created.written.length > 0 ? "installed" : "already",
|
|
34679
|
+
database: app.database,
|
|
34680
|
+
filesWritten: created.written,
|
|
34681
|
+
filesUnchanged: [...inspected.unchanged, ...created.unchanged],
|
|
34682
|
+
conflicts: [...inspected.conflicts, ...created.conflicts],
|
|
34683
|
+
tablesCreated: schema.tablesCreated,
|
|
34684
|
+
viewsRecreated: schema.viewsRecreated
|
|
34685
|
+
};
|
|
34686
|
+
}
|
|
34687
|
+
|
|
34688
|
+
// src/commands/apps.ts
|
|
34689
|
+
init_errors();
|
|
34690
|
+
init_formatter();
|
|
34691
|
+
init_theme();
|
|
34692
|
+
var APPLICATIONS_SPACE = "applications";
|
|
34693
|
+
function registerAppsCommand(program) {
|
|
34694
|
+
const apps = program.command("apps").description("Install catalog apps into your applications space");
|
|
34695
|
+
apps.command("install <app_id>").description("Install a pinned catalog app: verify Share bytes, apply its schema, write its knowledge bundle").addHelpText(
|
|
34696
|
+
"after",
|
|
34697
|
+
`
|
|
34698
|
+
Examples:
|
|
34699
|
+
$ ${CLI_NAME23} apps install xyz.tinycloud.sample.healthlog
|
|
34700
|
+
`
|
|
34701
|
+
).action(async (appIdInput, _options, cmd) => {
|
|
34702
|
+
try {
|
|
34703
|
+
const appId = assertKnowledgeAppId(appIdInput);
|
|
34704
|
+
const entry = pinnedApp(appId);
|
|
34705
|
+
if (!entry) throw new CLIError("UNKNOWN_APP", `No pinned catalog entry for ${appId}`, ExitCode.USAGE_ERROR);
|
|
34706
|
+
const ctx = await ProfileManager.resolveContext(cmd.optsWithGlobals());
|
|
34707
|
+
const node = await ensureAuthenticated(ctx);
|
|
34708
|
+
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
34709
|
+
const spaceUri = operationSpaceResolver2(node, profile.spaceId)(APPLICATIONS_SPACE);
|
|
34710
|
+
const io = appsInstallIo(node.kvForSpace(spaceUri), node.sqlForSpace(spaceUri).db(entry.database));
|
|
34711
|
+
const result = await withSpinner(`Installing ${appId}...`, () => installApp(appId, io)).catch((error) => {
|
|
34712
|
+
throw installFailure(error);
|
|
34713
|
+
});
|
|
34714
|
+
if (shouldOutputJson()) outputJson(result);
|
|
34715
|
+
else printResult(result);
|
|
34716
|
+
} catch (error) {
|
|
34717
|
+
return handleError(error);
|
|
34718
|
+
}
|
|
34719
|
+
});
|
|
34720
|
+
}
|
|
34721
|
+
function appsInstallIo(kv, db) {
|
|
34722
|
+
return {
|
|
34723
|
+
async fetchBytes(url) {
|
|
34724
|
+
const response = await fetch(url, { redirect: "error" });
|
|
34725
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
34726
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
34727
|
+
},
|
|
34728
|
+
async kvGet(key) {
|
|
34729
|
+
const result = await kv.get(key, { binary: true });
|
|
34730
|
+
if (result.ok) {
|
|
34731
|
+
const bytes = result.data.data;
|
|
34732
|
+
if (bytes instanceof Uint8Array) return bytes;
|
|
34733
|
+
throw new Error("KV_GET_NOT_BYTES");
|
|
34734
|
+
}
|
|
34735
|
+
if (result.error.code === "KV_NOT_FOUND") return void 0;
|
|
34736
|
+
throw new Error(result.error.code ?? "KV_GET_FAILED");
|
|
34737
|
+
},
|
|
34738
|
+
async kvCreate(key, bytes) {
|
|
34739
|
+
const result = await kv.put(key, bytes, { ifNoneMatch: "*" });
|
|
34740
|
+
if (result.ok) return "created";
|
|
34741
|
+
if (result.error.code === "KV_PRECONDITION_FAILED") return "exists";
|
|
34742
|
+
throw new Error(result.error.code ?? "KV_PUT_FAILED");
|
|
34743
|
+
},
|
|
34744
|
+
async sqlExecute(statement) {
|
|
34745
|
+
const result = await db.execute(statement);
|
|
34746
|
+
if (!result.ok) throw new Error(result.error.code ?? "SQL_EXECUTE_FAILED");
|
|
34747
|
+
},
|
|
34748
|
+
async sqlTableNames() {
|
|
34749
|
+
const result = await db.query("SELECT name FROM sqlite_master WHERE type = 'table'");
|
|
34750
|
+
if (!result.ok) throw new Error(result.error.code ?? "SQL_QUERY_FAILED");
|
|
34751
|
+
return result.data.rows.map(tableName);
|
|
34752
|
+
}
|
|
34753
|
+
};
|
|
34754
|
+
}
|
|
34755
|
+
function tableName(row) {
|
|
34756
|
+
if (Array.isArray(row)) return String(row[0]);
|
|
34757
|
+
if (row !== null && typeof row === "object" && "name" in row) return String(row.name);
|
|
34758
|
+
throw new Error("SQL_ROW_UNEXPECTED");
|
|
34759
|
+
}
|
|
34760
|
+
function installFailure(error) {
|
|
34761
|
+
if (!(error instanceof AppsInstallError)) return error;
|
|
34762
|
+
return new CLIError(error.code, error.message, ExitCode.ERROR);
|
|
34763
|
+
}
|
|
34764
|
+
function printResult(result) {
|
|
34765
|
+
const rows = [
|
|
34766
|
+
["App", result.appId],
|
|
34767
|
+
["Status", result.status],
|
|
34768
|
+
["Database", result.database],
|
|
34769
|
+
["Tables created", result.tablesCreated.join(", ") || "none"],
|
|
34770
|
+
["Views recreated", result.viewsRecreated.join(", ") || "none"],
|
|
34771
|
+
["Knowledge written", String(result.filesWritten.length)],
|
|
34772
|
+
["Knowledge unchanged", String(result.filesUnchanged.length)],
|
|
34773
|
+
["Knowledge left as edited", String(result.conflicts.length)]
|
|
34774
|
+
];
|
|
34775
|
+
process.stdout.write(formatTable(["Field", "Value"], rows) + "\n");
|
|
34776
|
+
if (result.conflicts.length > 0) {
|
|
34777
|
+
const count = result.conflicts.length;
|
|
34778
|
+
const sentence = count === 1 ? "1 note was left as you edited it" : `${count} notes were left as you edited them`;
|
|
34779
|
+
process.stdout.write(theme.muted(`${sentence}: ${result.conflicts.join(", ")}`) + "\n");
|
|
34780
|
+
}
|
|
34781
|
+
if (result.status === "already") {
|
|
34782
|
+
process.stdout.write(theme.muted("Already installed; views refreshed.") + "\n");
|
|
34783
|
+
}
|
|
34784
|
+
}
|
|
34785
|
+
|
|
34315
34786
|
// src/commands/init.ts
|
|
34316
34787
|
init_profiles();
|
|
34317
34788
|
init_formatter();
|
|
34318
34789
|
init_errors();
|
|
34319
34790
|
init_constants();
|
|
34320
|
-
import { CLI_NAME as
|
|
34791
|
+
import { CLI_NAME as CLI_NAME24 } from "@tinycloud/operations/cli-name";
|
|
34321
34792
|
function registerInitCommand(program) {
|
|
34322
34793
|
program.command("init").description("Initialize a new TinyCloud profile").option("--name <profile>", "Profile name", "default").option("--key-only", "Only generate key, skip authentication").option("--host <url>", "TinyCloud node URL").option("--paste", "Use manual paste mode for authentication").option("--no-popup", "Print the OpenKey URL without opening a browser").option("--default-space <name>", "Default space used when --space is omitted (e.g. applications)").action(async (options, cmd) => {
|
|
34323
34794
|
try {
|
|
@@ -34335,7 +34806,7 @@ function registerInitCommand(program) {
|
|
|
34335
34806
|
if (await ProfileManager.profileExists(profileName2)) {
|
|
34336
34807
|
throw new CLIError(
|
|
34337
34808
|
"PROFILE_EXISTS",
|
|
34338
|
-
`Profile "${profileName2}" already exists. Use \`${
|
|
34809
|
+
`Profile "${profileName2}" already exists. Use \`${CLI_NAME24} profile delete ${profileName2}\` first or choose a different name.`,
|
|
34339
34810
|
ExitCode.ERROR
|
|
34340
34811
|
);
|
|
34341
34812
|
}
|
|
@@ -34391,30 +34862,7 @@ init_session_expired();
|
|
|
34391
34862
|
init_constants();
|
|
34392
34863
|
init_profiles();
|
|
34393
34864
|
import { invokeOperation as invokeOperation2 } from "@tinycloud/operations";
|
|
34394
|
-
import { CLI_NAME as
|
|
34395
|
-
|
|
34396
|
-
// src/lib/knowledge-input.ts
|
|
34397
|
-
init_constants();
|
|
34398
|
-
init_errors();
|
|
34399
|
-
init_safe_text();
|
|
34400
|
-
function usage2(message) {
|
|
34401
|
-
return new CLIError("INVALID_ARGUMENT", message, ExitCode.USAGE_ERROR);
|
|
34402
|
-
}
|
|
34403
|
-
function assertKnowledgeAppId(value) {
|
|
34404
|
-
if (!isAppId(value)) throw usage2(`Invalid app id "${safeClipped(value)}": ${APP_ID_RULE}.`);
|
|
34405
|
-
return value;
|
|
34406
|
-
}
|
|
34407
|
-
function assertKnowledgeConcept(value) {
|
|
34408
|
-
const reason = value.length > 300 ? "it is longer than 300 characters" : value.startsWith("/") ? "it starts with a slash" : /[*\\]/.test(value) ? "it contains a wildcard or backslash" : /%(?:2e|2f|5c)/i.test(value) ? "it contains an encoded dot, slash or backslash" : value.split("/").some((segment) => segment === "" || segment === "." || segment === "..") ? "it has an empty, `.` or `..` segment" : void 0;
|
|
34409
|
-
if (reason !== void 0) {
|
|
34410
|
-
throw usage2(
|
|
34411
|
-
`Invalid concept "${safeClipped(value)}": ${reason}. Use a path inside the app's knowledge folder, such as app or tables/meal.md.`
|
|
34412
|
-
);
|
|
34413
|
-
}
|
|
34414
|
-
return value;
|
|
34415
|
-
}
|
|
34416
|
-
|
|
34417
|
-
// src/commands/knowledge.ts
|
|
34865
|
+
import { CLI_NAME as CLI_NAME25 } from "@tinycloud/operations/cli-name";
|
|
34418
34866
|
init_permissions();
|
|
34419
34867
|
init_errors();
|
|
34420
34868
|
init_formatter();
|
|
@@ -34517,7 +34965,7 @@ function knowledgeFailure(result, profile, appId) {
|
|
|
34517
34965
|
case "error": {
|
|
34518
34966
|
if (result.error.code === "SESSION_NOT_FOUND" || result.error.code === "PROFILE_POSTURE_NOT_ALLOWED" && result.context.posture === "unauthenticated") {
|
|
34519
34967
|
return new CLIError("AUTH_REQUIRED", "Not signed in to TinyCloud.", ExitCode.AUTH_REQUIRED, {
|
|
34520
|
-
hint: `Sign in with: ${
|
|
34968
|
+
hint: `Sign in with: ${CLI_NAME25} --profile ${result.context.profile} auth login`
|
|
34521
34969
|
});
|
|
34522
34970
|
}
|
|
34523
34971
|
if (result.error.code === "NODE_UNREACHABLE") {
|
|
@@ -34547,7 +34995,7 @@ function knowledgeFailure(result, profile, appId) {
|
|
|
34547
34995
|
}
|
|
34548
34996
|
}
|
|
34549
34997
|
function missingGrantHint(profile, appId) {
|
|
34550
|
-
const request = `${
|
|
34998
|
+
const request = `${CLI_NAME25} --profile ${profile} auth request`;
|
|
34551
34999
|
const lines = appId === void 0 ? [
|
|
34552
35000
|
` ${request} --preset ${APPS_AGENT_PRESET} --emit req.json (read-only, all installed apps)`,
|
|
34553
35001
|
` ${request} --cap tinycloud.kv:${APPS_AGENT_SPACE}::get,list --emit req.json (all files, no table access)`,
|
|
@@ -34560,7 +35008,7 @@ function missingGrantHint(profile, appId) {
|
|
|
34560
35008
|
return [
|
|
34561
35009
|
"Ask the owner for a grant, narrowest first. Create the request:",
|
|
34562
35010
|
...lines,
|
|
34563
|
-
`The owner runs \`${
|
|
35011
|
+
`The owner runs \`${CLI_NAME25} auth grant req.json > grant.json\`; then import the result with \`${CLI_NAME25} --profile ${profile} auth import grant.json\`.`
|
|
34564
35012
|
].join("\n");
|
|
34565
35013
|
}
|
|
34566
35014
|
|
|
@@ -34868,7 +35316,7 @@ init_errors();
|
|
|
34868
35316
|
init_constants();
|
|
34869
35317
|
init_theme();
|
|
34870
35318
|
import { resolveManifestKnowledgeRoot as resolveManifestKnowledgeRoot2 } from "@tinycloud/sdk-core";
|
|
34871
|
-
import { CLI_NAME as
|
|
35319
|
+
import { CLI_NAME as CLI_NAME26 } from "@tinycloud/operations/cli-name";
|
|
34872
35320
|
import { readFile as readFile12 } from "fs/promises";
|
|
34873
35321
|
var DEFAULT_APP_SPACE = "applications";
|
|
34874
35322
|
function registerManifestCommand(program) {
|
|
@@ -34876,8 +35324,8 @@ function registerManifestCommand(program) {
|
|
|
34876
35324
|
manifest.command("resolve <source>").description("Resolve a manifest file or URL to its effective space, paths, and DB basenames").addHelpText("after", `
|
|
34877
35325
|
|
|
34878
35326
|
Examples:
|
|
34879
|
-
$ ${
|
|
34880
|
-
$ ${
|
|
35327
|
+
$ ${CLI_NAME26} manifest resolve ./manifest.json
|
|
35328
|
+
$ ${CLI_NAME26} manifest resolve https://app.example.com/manifest.json --json
|
|
34881
35329
|
|
|
34882
35330
|
What it shows:
|
|
34883
35331
|
- app_id, name, manifest_version
|
|
@@ -34887,7 +35335,7 @@ What it shows:
|
|
|
34887
35335
|
|
|
34888
35336
|
This command is read-only and does NOT contact the node \u2014 it just resolves
|
|
34889
35337
|
the manifest against the active profile's address/chain so you know which
|
|
34890
|
-
\`--space\` and \`--db\` values to pass to other ${
|
|
35338
|
+
\`--space\` and \`--db\` values to pass to other ${CLI_NAME26} commands.
|
|
34891
35339
|
`).action(async (source, _options, cmd) => {
|
|
34892
35340
|
try {
|
|
34893
35341
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -34956,7 +35404,7 @@ ${theme.heading("SQL databases")}
|
|
|
34956
35404
|
`);
|
|
34957
35405
|
}
|
|
34958
35406
|
process.stdout.write(theme.muted(`
|
|
34959
|
-
Use with: ${
|
|
35407
|
+
Use with: ${CLI_NAME26} sql query --space ${spaceName} --db <db> "..."
|
|
34960
35408
|
`));
|
|
34961
35409
|
}
|
|
34962
35410
|
if (permissions.length > 0) {
|
|
@@ -35076,7 +35524,7 @@ init_profiles();
|
|
|
35076
35524
|
init_formatter();
|
|
35077
35525
|
init_errors();
|
|
35078
35526
|
init_constants();
|
|
35079
|
-
import { CLI_NAME as
|
|
35527
|
+
import { CLI_NAME as CLI_NAME27 } from "@tinycloud/operations/cli-name";
|
|
35080
35528
|
import { createInterface as createInterface5 } from "readline";
|
|
35081
35529
|
init_theme();
|
|
35082
35530
|
init_types();
|
|
@@ -35222,9 +35670,9 @@ profile at command time. Precedence for every kv/sql command:
|
|
|
35222
35670
|
explicit --space flag > profile defaultSpace > primary space.
|
|
35223
35671
|
|
|
35224
35672
|
Examples:
|
|
35225
|
-
$ ${
|
|
35226
|
-
$ ${
|
|
35227
|
-
$ ${
|
|
35673
|
+
$ ${CLI_NAME27} profile set-default-space applications
|
|
35674
|
+
$ ${CLI_NAME27} profile set-default-space applications --profile cli-test
|
|
35675
|
+
$ ${CLI_NAME27} profile set-default-space --unset
|
|
35228
35676
|
`).action(async (name, options, cmd) => {
|
|
35229
35677
|
try {
|
|
35230
35678
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -35236,7 +35684,7 @@ Examples:
|
|
|
35236
35684
|
if (!options.unset && (name === void 0 || name === "")) {
|
|
35237
35685
|
throw new CLIError(
|
|
35238
35686
|
"USAGE_ERROR",
|
|
35239
|
-
`Provide a space name (e.g. \`${
|
|
35687
|
+
`Provide a space name (e.g. \`${CLI_NAME27} profile set-default-space applications\`) or pass --unset.`,
|
|
35240
35688
|
ExitCode.USAGE_ERROR
|
|
35241
35689
|
);
|
|
35242
35690
|
}
|
|
@@ -35295,7 +35743,7 @@ function parseOperatorType(raw) {
|
|
|
35295
35743
|
|
|
35296
35744
|
// src/commands/replica.ts
|
|
35297
35745
|
init_profile_replicas();
|
|
35298
|
-
import { CLI_NAME as
|
|
35746
|
+
import { CLI_NAME as CLI_NAME28 } from "@tinycloud/operations/cli-name";
|
|
35299
35747
|
import { randomBytes as randomBytes7 } from "crypto";
|
|
35300
35748
|
import { constants as fsConstants5 } from "fs";
|
|
35301
35749
|
import { mkdir as mkdir5, open as open11, readdir as readdir6, rename as rename4, unlink as unlink5, lstat as lstat9 } from "fs/promises";
|
|
@@ -35436,11 +35884,11 @@ var EXIT_BY_CODE = {
|
|
|
35436
35884
|
var READ_FAILURE = {
|
|
35437
35885
|
absent: { code: "KEY_ABSENT", exit: ExitCode.NOT_FOUND, what: "is not in the replica (the replica covers its whole prefix)" },
|
|
35438
35886
|
deleted: { code: "KEY_DELETED", exit: ExitCode.NOT_FOUND, what: "was deleted at the source" },
|
|
35439
|
-
content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${
|
|
35887
|
+
content_missing: { code: "CONTENT_MISSING", exit: ExitCode.NOT_FOUND, what: `is known but its content has not been fetched yet; run ${CLI_NAME28} replica sync` },
|
|
35440
35888
|
coverage_incomplete: {
|
|
35441
35889
|
code: "COVERAGE_INCOMPLETE",
|
|
35442
35890
|
exit: ExitCode.NOT_FOUND,
|
|
35443
|
-
what: `is not in the replica, which has not finished its first sync; run ${
|
|
35891
|
+
what: `is not in the replica, which has not finished its first sync; run ${CLI_NAME28} replica sync`
|
|
35444
35892
|
},
|
|
35445
35893
|
not_covered: { code: "NOT_COVERED", exit: ExitCode.USAGE_ERROR, what: "is outside the replica's prefix" }
|
|
35446
35894
|
};
|
|
@@ -35558,7 +36006,7 @@ async function existingReplicaName(profile, option) {
|
|
|
35558
36006
|
const names = await replicaNames(profile);
|
|
35559
36007
|
if (names.length === 1) return names[0];
|
|
35560
36008
|
if (names.length === 0) {
|
|
35561
|
-
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${
|
|
36009
|
+
throw new ReplicaError(ReplicaErrorCode.NOT_FOUND, `Profile "${profile}" has no replicas. Create one with: ${CLI_NAME28} replica sync --prefix <prefix>`);
|
|
35562
36010
|
}
|
|
35563
36011
|
throw new CLIError("USAGE_ERROR", `Profile "${profile}" has several replicas (${names.join(", ")}); pass --replica <name>.`, ExitCode.USAGE_ERROR);
|
|
35564
36012
|
}
|
|
@@ -35666,9 +36114,9 @@ async function storedGrants(profile) {
|
|
|
35666
36114
|
function grantHint(space, prefix) {
|
|
35667
36115
|
return [
|
|
35668
36116
|
"A replica needs a device grant with get and sync on its prefix:",
|
|
35669
|
-
` device: ${
|
|
35670
|
-
` owner: ${
|
|
35671
|
-
` device: ${
|
|
36117
|
+
` device: ${CLI_NAME28} auth request --cap tinycloud.kv:${space}:${prefix}:get,list,metadata,sync --expiry 30d --emit req.json`,
|
|
36118
|
+
` owner: ${CLI_NAME28} auth grant req.json > grant.json`,
|
|
36119
|
+
` device: ${CLI_NAME28} auth import grant.json`
|
|
35672
36120
|
].join("\n");
|
|
35673
36121
|
}
|
|
35674
36122
|
function resolveSpace(input, prefix, grants) {
|
|
@@ -35759,7 +36207,7 @@ function describeStatus(status) {
|
|
|
35759
36207
|
formatField("Size", formatBytes(status.bytes)),
|
|
35760
36208
|
formatField("Grant", status.device.delegationCid),
|
|
35761
36209
|
formatField("Last error", status.lastError ? `${status.lastError.code}: ${status.lastError.message}` : null),
|
|
35762
|
-
formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${
|
|
36210
|
+
formatField("Purge", status.purgePending ? `incomplete: content files remain; the next ${CLI_NAME28} replica command retries` : null)
|
|
35763
36211
|
].join("\n");
|
|
35764
36212
|
}
|
|
35765
36213
|
async function writeFileAtomic(path, bytes) {
|
|
@@ -35923,7 +36371,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
|
35923
36371
|
(options, cmd) => run(async () => {
|
|
35924
36372
|
const profile = await profileName(cmd);
|
|
35925
36373
|
if (options.purge) {
|
|
35926
|
-
throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${
|
|
36374
|
+
throw new CLIError("REPORT_READ_ONLY", `Replica report is read-only. To remove a named local replica, run ${CLI_NAME28} replica reset --replica <name> --purge; flag-owned purge has no equivalent yet (TC-1309).`, ExitCode.USAGE_ERROR);
|
|
35927
36375
|
}
|
|
35928
36376
|
if (options.clearPending) {
|
|
35929
36377
|
throw new CLIError("REPORT_READ_ONLY", "Clearing pending replica writes has no equivalent yet; see TC-1309 for a safe replacement command.", ExitCode.USAGE_ERROR);
|
|
@@ -36025,7 +36473,7 @@ or coverage incomplete; 5 grant expired, revoked or not yet valid; 6 network;
|
|
|
36025
36473
|
if (differs !== void 0) {
|
|
36026
36474
|
throw new ReplicaError(
|
|
36027
36475
|
ReplicaErrorCode.CONFIG_MISMATCH,
|
|
36028
|
-
`Replica ${name} already follows ${differs}. Use another --replica name, or ${
|
|
36476
|
+
`Replica ${name} already follows ${differs}. Use another --replica name, or ${CLI_NAME28} replica reset --replica ${name} --purge first.`
|
|
36029
36477
|
);
|
|
36030
36478
|
}
|
|
36031
36479
|
}
|
|
@@ -36190,7 +36638,7 @@ init_theme();
|
|
|
36190
36638
|
init_errors();
|
|
36191
36639
|
init_warnings();
|
|
36192
36640
|
init_constants();
|
|
36193
|
-
import { CLI_NAME as
|
|
36641
|
+
import { CLI_NAME as CLI_NAME30 } from "@tinycloud/operations/cli-name";
|
|
36194
36642
|
import { readFile as readFile13 } from "fs/promises";
|
|
36195
36643
|
import { join as join17 } from "path";
|
|
36196
36644
|
import { homedir, hostname, userInfo } from "os";
|
|
@@ -36401,7 +36849,7 @@ function parseStatusEnvelope(line) {
|
|
|
36401
36849
|
// src/lib/secrets-wait.ts
|
|
36402
36850
|
init_constants();
|
|
36403
36851
|
init_errors();
|
|
36404
|
-
import { CLI_NAME as
|
|
36852
|
+
import { CLI_NAME as CLI_NAME29 } from "@tinycloud/operations/cli-name";
|
|
36405
36853
|
var WAIT_BACKOFF_MS = [3e3, 4500, 6750, 10125];
|
|
36406
36854
|
var WAIT_STEADY_INTERVAL_MS = 15e3;
|
|
36407
36855
|
var DEFAULT_WAIT_TIMEOUT = "5m";
|
|
@@ -36409,7 +36857,7 @@ var MAX_WAIT_TIMEOUT_MS = 9 * 6e4;
|
|
|
36409
36857
|
var MAX_CONSECUTIVE_FAILURES = 5;
|
|
36410
36858
|
var TIMEOUT_RE = /^(0|\d+s|\d+m)$/;
|
|
36411
36859
|
var RETRYABLE_STATUS_ERRORS = { NODE_UNREACHABLE: true, NODE_ERROR: true };
|
|
36412
|
-
var EXPLICIT_KEY_HINT = `${
|
|
36860
|
+
var EXPLICIT_KEY_HINT = `${CLI_NAME29} secrets wait never signs in, and an explicit private key has no stored session. Use a local profile signed in once with ${CLI_NAME29} --profile <profile> auth login --method local, without TC_PRIVATE_KEY.`;
|
|
36413
36861
|
function abortableSleep(ms2, signal) {
|
|
36414
36862
|
return new Promise((resolve9) => {
|
|
36415
36863
|
if (signal.aborted) {
|
|
@@ -36445,7 +36893,7 @@ async function runSecretsWait(request, deps) {
|
|
|
36445
36893
|
let step = 0;
|
|
36446
36894
|
const cancelled = () => new CLIError(
|
|
36447
36895
|
"CANCELLED",
|
|
36448
|
-
`Interrupted: ${
|
|
36896
|
+
`Interrupted: ${CLI_NAME29} secrets wait stopped before the secrets were ready.`,
|
|
36449
36897
|
ExitCode.CANCELLED,
|
|
36450
36898
|
progress === void 0 ? {} : { progress }
|
|
36451
36899
|
);
|
|
@@ -36589,13 +37037,13 @@ function statusError(error, profile, warnings) {
|
|
|
36589
37037
|
if (error.code === "AUTH_REQUIRED" || error.code === "SESSION_NOT_FOUND") {
|
|
36590
37038
|
const recovery = recoveryOf(error, "sign_in");
|
|
36591
37039
|
if (recovery !== void 0) {
|
|
36592
|
-
const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${
|
|
37040
|
+
const hint = recovery.command !== void 0 ? `Sign in once with: ${recovery.command}, then retry.` : `Sign in to profile "${profile}" once with ${CLI_NAME29} auth login, then retry.`;
|
|
36593
37041
|
return new CLIError("AUTH_REQUIRED", error.message, ExitCode.AUTH_REQUIRED, { hint, recovery, ...extra });
|
|
36594
37042
|
}
|
|
36595
37043
|
}
|
|
36596
37044
|
if (error.code === "PROFILE_MIGRATION_REQUIRED") {
|
|
36597
37045
|
const recovery = recoveryOf(error, "complete_migration");
|
|
36598
|
-
const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${
|
|
37046
|
+
const hint = recovery?.command !== void 0 ? `Run ${recovery.command} once to finish this profile's one-time upgrade, then retry wait.` : `Run ${CLI_NAME29} auth caps once with profile "${profile}" to finish its one-time upgrade, then retry wait.`;
|
|
36599
37047
|
return new CLIError("PROFILE_MIGRATION_REQUIRED", error.message, ExitCode.ERROR, {
|
|
36600
37048
|
hint,
|
|
36601
37049
|
...recovery === void 0 ? {} : { recovery },
|
|
@@ -36787,11 +37235,11 @@ async function runSecretOperation(params) {
|
|
|
36787
37235
|
}
|
|
36788
37236
|
function secretPermissionReason(action, name) {
|
|
36789
37237
|
const target = name ? ` secret "${name}"` : " secrets";
|
|
36790
|
-
return `Allow \`${
|
|
37238
|
+
return `Allow \`${CLI_NAME30} secrets ${action}${name ? ` ${name}` : ""}\` to access${target} with the required TinyCloud permissions.`;
|
|
36791
37239
|
}
|
|
36792
37240
|
function assertOwnerApprovalPossible(profileName2, profile, action, name, missing = []) {
|
|
36793
37241
|
if (profile.authMethod !== "openkey" || process.stdin.isTTY || process.stderr.isTTY) return;
|
|
36794
|
-
const command = `${
|
|
37242
|
+
const command = `${CLI_NAME30} secrets ${action === "del" ? "delete" : action}${name ? ` ${name}` : ""}`;
|
|
36795
37243
|
throw new CLIError(
|
|
36796
37244
|
"PERMISSION_DENIED",
|
|
36797
37245
|
action === "get" ? missing.length > 0 && missing.every((permission) => permission.service === "tinycloud.encryption" && Array.isArray(permission.actions) && permission.actions.length > 0 && permission.actions.every((capability) => capability === SECRET_DECRYPT_CAPABILITY)) ? `Profile "${profileName2}" lacks the scoped decrypt authority (${SECRET_DECRYPT_CAPABILITY}) needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName2}" lacks a scoped read or decrypt grant needed to read secret "${name}", and requesting it needs an interactive browser approval.` : `Profile "${profileName2}" holds no grant for \`${command}\`, and requesting one needs an interactive browser approval.`,
|
|
@@ -36802,7 +37250,7 @@ function assertOwnerApprovalPossible(profileName2, profile, action, name, missin
|
|
|
36802
37250
|
);
|
|
36803
37251
|
}
|
|
36804
37252
|
function scopedSecretLoginHint(profileName2) {
|
|
36805
|
-
return `Have the owner approve a scoped login whose manifest names the secret: ${
|
|
37253
|
+
return `Have the owner approve a scoped login whose manifest names the secret: ${CLI_NAME30} --profile ${profileName2} auth login --method openkey --paste --manifest <manifest.json>. Pass the owner's code on stdin, newline-terminated.`;
|
|
36806
37254
|
}
|
|
36807
37255
|
async function runSecretOperationAttempt(label, operation) {
|
|
36808
37256
|
try {
|
|
@@ -36936,7 +37384,7 @@ ${message}`,
|
|
|
36936
37384
|
"AUTH_REQUIRED",
|
|
36937
37385
|
"Not signed in to TinyCloud.",
|
|
36938
37386
|
ExitCode.AUTH_REQUIRED,
|
|
36939
|
-
{ hint: `Sign in with: ${
|
|
37387
|
+
{ hint: `Sign in with: ${CLI_NAME30} --profile ${result.context.profile} auth login` }
|
|
36940
37388
|
);
|
|
36941
37389
|
}
|
|
36942
37390
|
if (result.error.code === "NODE_UNREACHABLE") {
|
|
@@ -37413,7 +37861,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
37413
37861
|
name: "Encryption network",
|
|
37414
37862
|
ok: false,
|
|
37415
37863
|
detail: `${networkName} not found`,
|
|
37416
|
-
hint: `${
|
|
37864
|
+
hint: `${CLI_NAME30} secrets network init ${networkName}`
|
|
37417
37865
|
}
|
|
37418
37866
|
];
|
|
37419
37867
|
let secret;
|
|
@@ -37459,7 +37907,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
37459
37907
|
name: "Secret access",
|
|
37460
37908
|
ok: false,
|
|
37461
37909
|
detail: notFound ? `${resolved.permissionPaths.vault} not found` : result.error.message,
|
|
37462
|
-
hint: notFound ? local ? `${
|
|
37910
|
+
hint: notFound ? local ? `${CLI_NAME30} secrets put ${resolved.name}${formatSecretScopeFlag(scopeOptions)} --stdin` : `${CLI_NAME30} secrets request ${resolved.name}${formatSecretScopeFlag(scopeOptions)}` : `Ask the owner profile to grant ${secretCapabilityAction("get")} and ${SECRET_DECRYPT_CAPABILITY}.`
|
|
37463
37911
|
});
|
|
37464
37912
|
}
|
|
37465
37913
|
} else {
|
|
@@ -37703,7 +38151,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
37703
38151
|
if (effectiveLocal(profile, options)) {
|
|
37704
38152
|
throw new CLIError(
|
|
37705
38153
|
"LOCAL_OWNER_PROFILE",
|
|
37706
|
-
`Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${
|
|
38154
|
+
`Profile "${ctx.profile}" uses a local key, which owns its secrets: store them with ${CLI_NAME30} secrets put NAME --stdin instead of requesting them.`,
|
|
37707
38155
|
ExitCode.USAGE_ERROR
|
|
37708
38156
|
);
|
|
37709
38157
|
}
|
|
@@ -37826,7 +38274,7 @@ function registerSecretsCommand(program, openKeyAcquisition) {
|
|
|
37826
38274
|
|
|
37827
38275
|
// src/commands/share.ts
|
|
37828
38276
|
import { Option } from "commander";
|
|
37829
|
-
import { CLI_NAME as
|
|
38277
|
+
import { CLI_NAME as CLI_NAME31 } from "@tinycloud/operations/cli-name";
|
|
37830
38278
|
import { ProfileLockTimeoutError as ProfileLockTimeoutError3 } from "@tinycloud/operations/state";
|
|
37831
38279
|
init_errors();
|
|
37832
38280
|
|
|
@@ -39220,7 +39668,7 @@ function htmlFolderNotice(bundle) {
|
|
|
39220
39668
|
}
|
|
39221
39669
|
|
|
39222
39670
|
// src/commands/share.ts
|
|
39223
|
-
var
|
|
39671
|
+
var SHARE_ORIGIN2 = "https://share.tinycloud.xyz";
|
|
39224
39672
|
var NOTIFY_WINDOW_MESSAGE = "Share notification authorization expired (at share expiry or 5 minutes after publication); publish a new share and invite the recipient then.\n";
|
|
39225
39673
|
var ONGOING_UNAVAILABLE = "Ongoing shares are not available yet; v1 shares pin a version";
|
|
39226
39674
|
function artifactRequest(options, actions, inputCount) {
|
|
@@ -39285,7 +39733,7 @@ function shareCliError(error, operation = "publish") {
|
|
|
39285
39733
|
"SHARE_HISTORY_UNREADABLE",
|
|
39286
39734
|
"sender history cannot be read with the current session key",
|
|
39287
39735
|
1,
|
|
39288
|
-
{ hint: `Run \`${
|
|
39736
|
+
{ hint: `Run \`${CLI_NAME31} share history recover\` to move the unreadable file aside. The old file is kept, not deleted.` }
|
|
39289
39737
|
);
|
|
39290
39738
|
}
|
|
39291
39739
|
if (error instanceof SharePublishAuthorityError) {
|
|
@@ -39293,12 +39741,12 @@ function shareCliError(error, operation = "publish") {
|
|
|
39293
39741
|
const profileName2 = "profileName" in failure ? failure.profileName : void 0;
|
|
39294
39742
|
const localKey = "localKey" in failure && failure.localKey === true;
|
|
39295
39743
|
const profileHint = profileName2 === void 0 ? "" : `--profile ${profileName2} `;
|
|
39296
|
-
const loginHint = localKey ? `\`${
|
|
39744
|
+
const loginHint = localKey ? `\`${CLI_NAME31} ${profileHint}auth login --method local\`` : `\`${CLI_NAME31} ${profileHint}auth login --device --manifest builtin:share-publishing\` (or \`${CLI_NAME31} ${profileHint}enable share\`)`;
|
|
39297
39745
|
if (failure.kind === "caveated-session") {
|
|
39298
39746
|
const holder = profileName2 === void 0 ? "this session" : `profile ${profileName2}'s session`;
|
|
39299
39747
|
return new CLIError(
|
|
39300
39748
|
"PERMISSION_DENIED",
|
|
39301
|
-
`${holder} carries signed restrictions (caveats) on the authority an anyone-with-link share needs, so it cannot create the share link; nothing was shared. Approve Share publishing without restrictions on a new, dedicated profile (any unused name): \`${
|
|
39749
|
+
`${holder} carries signed restrictions (caveats) on the authority an anyone-with-link share needs, so it cannot create the share link; nothing was shared. Approve Share publishing without restrictions on a new, dedicated profile (any unused name): \`${CLI_NAME31} init --name publisher --key-only && ${CLI_NAME31} --profile publisher enable share\``,
|
|
39302
39750
|
5
|
|
39303
39751
|
);
|
|
39304
39752
|
}
|
|
@@ -39384,7 +39832,7 @@ function shareCliError(error, operation = "publish") {
|
|
|
39384
39832
|
if (nodeCode === "ENOENT") return new CLIError("INVALID_ARGUMENT", "share input was not found", 2);
|
|
39385
39833
|
if (nodeCode === "EISDIR") return new CLIError("INVALID_ARGUMENT", "share input must be a Markdown file", 2);
|
|
39386
39834
|
const mapped = typeof nodeCode === "string" && known[nodeCode] !== void 0 ? known[nodeCode] : known[message];
|
|
39387
|
-
const classified = mapped === void 0 ? classifyShareFailure(error, `\`${
|
|
39835
|
+
const classified = mapped === void 0 ? classifyShareFailure(error, `\`${CLI_NAME31} auth login\``) : void 0;
|
|
39388
39836
|
if (classified !== void 0) return Object.assign(new CLIError(classified.code, classified.reason, classified.exitCode), { cause: error });
|
|
39389
39837
|
if (error instanceof TypeError) return new CLIError("INVALID_ARGUMENT", "share input is invalid", 2);
|
|
39390
39838
|
return new CLIError(mapped?.code ?? "INVALID_ARGUMENT", mapped ? mapped.code : "share operation failed", mapped?.exit ?? 2);
|
|
@@ -39473,7 +39921,7 @@ function assertAggregateInputLimit(inputs, maxBytes) {
|
|
|
39473
39921
|
}
|
|
39474
39922
|
function registerShareCommand(program) {
|
|
39475
39923
|
const share = program.command("share").description("Publish and consume TinyCloud Share links");
|
|
39476
|
-
share.command("publish <files...>").description("Publish one or more bounded files as a Share, or a folder with an index.html as one self-contained page (anyone links only)").option("--name <filename>", "Filename for stdin input, or the published .html name of a folder").option("--to <target>", "Share target", "anyone").option("--notify", "Email an exact-email recipient, or a domain mailbox on node 1.17.3+").option("--notify-to <address>", "Mailbox to invite for --to domain:<name> --notify (node 1.17.3+)").option("--expires <duration>", "Share lifetime").option("--max-bytes <bytes>", "Bound input bytes").option("--media-type <type>", "Media type for a single input").option("--action <actions...>", "Addressed permission: read, list, or edit").option("--prefix", "Publish multiple inputs beneath one addressed prefix").option("--binary", "Allow non-UTF-8 bearer content").option("--artifact <id>", 'Publish as the next write-once version of an artifact ("new" starts one); the share is pinned to that version').addOption(new Option("--ongoing").hideHelp()).option("--note <text>", "Note stored with the artifact version").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Canonical HTTPS viewer origin",
|
|
39924
|
+
share.command("publish <files...>").description("Publish one or more bounded files as a Share, or a folder with an index.html as one self-contained page (anyone links only)").option("--name <filename>", "Filename for stdin input, or the published .html name of a folder").option("--to <target>", "Share target", "anyone").option("--notify", "Email an exact-email recipient, or a domain mailbox on node 1.17.3+").option("--notify-to <address>", "Mailbox to invite for --to domain:<name> --notify (node 1.17.3+)").option("--expires <duration>", "Share lifetime").option("--max-bytes <bytes>", "Bound input bytes").option("--media-type <type>", "Media type for a single input").option("--action <actions...>", "Addressed permission: read, list, or edit").option("--prefix", "Publish multiple inputs beneath one addressed prefix").option("--binary", "Allow non-UTF-8 bearer content").option("--artifact <id>", 'Publish as the next write-once version of an artifact ("new" starts one); the share is pinned to that version').addOption(new Option("--ongoing").hideHelp()).option("--note <text>", "Note stored with the artifact version").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Canonical HTTPS viewer origin", SHARE_ORIGIN2).action(async (files, options, command) => {
|
|
39477
39925
|
try {
|
|
39478
39926
|
const json = jsonOutput(options, command);
|
|
39479
39927
|
if (options.ongoing === true) throw new CLIError("UNSUPPORTED_TARGET", ONGOING_UNAVAILABLE, 2);
|
|
@@ -39564,7 +40012,7 @@ function registerShareCommand(program) {
|
|
|
39564
40012
|
return handleError(shareCliError(error));
|
|
39565
40013
|
}
|
|
39566
40014
|
});
|
|
39567
|
-
share.command("inspect [url]").description("Verify a share link and print safe metadata").option("--stdin", "Read the complete URL from stdin").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin",
|
|
40015
|
+
share.command("inspect [url]").description("Verify a share link and print safe metadata").option("--stdin", "Read the complete URL from stdin").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN2).action(async (url, options, command) => {
|
|
39568
40016
|
try {
|
|
39569
40017
|
const json = jsonOutput(options, command);
|
|
39570
40018
|
const result = await inspectShareInputOnce(url, options.stdin === true, options.viewerOrigin);
|
|
@@ -39609,7 +40057,7 @@ function registerShareCommand(program) {
|
|
|
39609
40057
|
return handleError(shareCliError(error));
|
|
39610
40058
|
}
|
|
39611
40059
|
});
|
|
39612
|
-
share.command("receive [url]").description("Verify and receive a share link").option("--stdin", "Read the complete URL from stdin").option("--output <directory>", "Create the file in this directory").option("--stdout", "Write verified plaintext bytes to stdout").option("--force", "Allow replacing an existing non-symlink output").option("--max-bytes <bytes>", "Bound received content bytes").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin",
|
|
40060
|
+
share.command("receive [url]").description("Verify and receive a share link").option("--stdin", "Read the complete URL from stdin").option("--output <directory>", "Create the file in this directory").option("--stdout", "Write verified plaintext bytes to stdout").option("--force", "Allow replacing an existing non-symlink output").option("--max-bytes <bytes>", "Bound received content bytes").option("--json", "Print versioned redacted JSON").option("--viewer-origin <origin>", "Require this canonical Share origin", SHARE_ORIGIN2).action(async (url, options, command) => {
|
|
39613
40061
|
try {
|
|
39614
40062
|
const json = jsonOutput(options, command);
|
|
39615
40063
|
if (options.stdout && json) throw new CLIError("INVALID_ARGUMENT", "--stdout and --json are mutually exclusive", 2);
|
|
@@ -39748,7 +40196,7 @@ init_constants();
|
|
|
39748
40196
|
init_sdk();
|
|
39749
40197
|
init_host();
|
|
39750
40198
|
init_theme();
|
|
39751
|
-
import { CLI_NAME as
|
|
40199
|
+
import { CLI_NAME as CLI_NAME32 } from "@tinycloud/operations/cli-name";
|
|
39752
40200
|
import { ACCOUNT_REGISTRY_SPACE } from "@tinycloud/node-sdk";
|
|
39753
40201
|
import { randomBytes as randomBytes9 } from "crypto";
|
|
39754
40202
|
import { mkdir as mkdir7, writeFile as writeFile4 } from "fs/promises";
|
|
@@ -39795,7 +40243,7 @@ function registerSpaceCommand(program) {
|
|
|
39795
40243
|
} else {
|
|
39796
40244
|
const profile = await ProfileManager.getProfile(ctx.profile);
|
|
39797
40245
|
const cap = "tinycloud.kv:account:spaces/:get,list";
|
|
39798
|
-
const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${
|
|
40246
|
+
const hint = resolveProfilePosture(profile) === "delegate-session" ? `Ask the space owner to grant it. Create the request with: ${CLI_NAME32} --profile ${ctx.profile} auth request --cap ${cap} --emit` : `Grant it to this session with: ${CLI_NAME32} --profile ${ctx.profile} auth request --grant --cap ${cap}. The default OpenKey owner sign-in covers only the default space, so it includes neither.`;
|
|
39799
40247
|
throw new CLIError(
|
|
39800
40248
|
"PERMISSION_DENIED",
|
|
39801
40249
|
`Listing spaces needs tinycloud.kv/get and tinycloud.kv/list on the account space's spaces/ registry (${accountSpaceId ?? ACCOUNT_REGISTRY_SPACE}), or tinycloud.space/list. This session holds neither.`,
|
|
@@ -39832,10 +40280,10 @@ function registerSpaceCommand(program) {
|
|
|
39832
40280
|
|
|
39833
40281
|
A delegate cannot host a space \u2014 only its owner (root authority) can. This
|
|
39834
40282
|
emits a tinycloud.host.request artifact naming the space and its owner so you
|
|
39835
|
-
can hand it to the owner; they run \`${
|
|
40283
|
+
can hand it to the owner; they run \`${CLI_NAME32} space host <name>\` and confirm.
|
|
39836
40284
|
|
|
39837
40285
|
If you ARE the owner of the resolved space, this refuses and tells you to host
|
|
39838
|
-
it directly with \`${
|
|
40286
|
+
it directly with \`${CLI_NAME32} space host <name>\` (no request needed).
|
|
39839
40287
|
`).action(async (name, options, cmd) => {
|
|
39840
40288
|
try {
|
|
39841
40289
|
const globalOpts = cmd.optsWithGlobals();
|
|
@@ -39846,7 +40294,7 @@ it directly with \`${CLI_NAME31} space host <name>\` (no request needed).
|
|
|
39846
40294
|
if (await isRootAuthority(spaceId, ctx.profile)) {
|
|
39847
40295
|
throw new CLIError(
|
|
39848
40296
|
"ALREADY_ROOT_AUTHORITY",
|
|
39849
|
-
`You are the owner of ${spaceId}. Host it directly: ${
|
|
40297
|
+
`You are the owner of ${spaceId}. Host it directly: ${CLI_NAME32} space host ${spaceName}`,
|
|
39850
40298
|
ExitCode.USAGE_ERROR
|
|
39851
40299
|
);
|
|
39852
40300
|
}
|
|
@@ -39932,7 +40380,7 @@ init_sdk();
|
|
|
39932
40380
|
init_space();
|
|
39933
40381
|
init_host();
|
|
39934
40382
|
init_theme();
|
|
39935
|
-
import { CLI_NAME as
|
|
40383
|
+
import { CLI_NAME as CLI_NAME33 } from "@tinycloud/operations/cli-name";
|
|
39936
40384
|
import { writeFile as writeFile5 } from "fs/promises";
|
|
39937
40385
|
import { resolve as resolve8 } from "path";
|
|
39938
40386
|
async function dbHandle(node, dbName, spaceInput, profileName2) {
|
|
@@ -39956,12 +40404,12 @@ database for simple apps, or pass --db to target a named database. Pass
|
|
|
39956
40404
|
--space to target a non-primary space (e.g. the manifest "applications" space).
|
|
39957
40405
|
|
|
39958
40406
|
Common workflows:
|
|
39959
|
-
$ ${
|
|
39960
|
-
$ ${
|
|
39961
|
-
$ ${
|
|
39962
|
-
$ ${
|
|
39963
|
-
$ ${
|
|
39964
|
-
$ ${
|
|
40407
|
+
$ ${CLI_NAME33} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
|
|
40408
|
+
$ ${CLI_NAME33} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["ship docs"]'
|
|
40409
|
+
$ ${CLI_NAME33} sql query "SELECT id, body FROM notes ORDER BY id"
|
|
40410
|
+
$ ${CLI_NAME33} sql query "SELECT * FROM events WHERE type = ?" --db analytics --params '["signup"]'
|
|
40411
|
+
$ ${CLI_NAME33} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
|
|
40412
|
+
$ ${CLI_NAME33} sql export --db analytics --output analytics.db
|
|
39965
40413
|
|
|
39966
40414
|
Commands:
|
|
39967
40415
|
query Read rows with SELECT statements
|
|
@@ -39978,10 +40426,10 @@ Tips:
|
|
|
39978
40426
|
sql.command("query <sql>").description("Run a read-only SELECT query").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").addHelpText("after", `
|
|
39979
40427
|
|
|
39980
40428
|
Examples:
|
|
39981
|
-
$ ${
|
|
39982
|
-
$ ${
|
|
39983
|
-
$ ${
|
|
39984
|
-
$ ${
|
|
40429
|
+
$ ${CLI_NAME33} sql query "SELECT * FROM notes ORDER BY id"
|
|
40430
|
+
$ ${CLI_NAME33} sql query "SELECT * FROM notes WHERE id = ?" --params '[42]'
|
|
40431
|
+
$ ${CLI_NAME33} sql query "SELECT count(*) AS total FROM events" --db analytics --json
|
|
40432
|
+
$ ${CLI_NAME33} sql query "SELECT count(*) FROM conversation" --space applications --db xyz.tinycloud.listen/conversations
|
|
39985
40433
|
|
|
39986
40434
|
Output:
|
|
39987
40435
|
Human output is formatted as a table. Piped output or --json returns
|
|
@@ -40022,11 +40470,11 @@ ${rowCount} row${rowCount === 1 ? "" : "s"} returned`) + "\n");
|
|
|
40022
40470
|
sql.command("execute <sql>").description("Run a write or schema statement").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("--params <json>", "Bind parameters as a JSON array for ? placeholders").addHelpText("after", `
|
|
40023
40471
|
|
|
40024
40472
|
Examples:
|
|
40025
|
-
$ ${
|
|
40026
|
-
$ ${
|
|
40027
|
-
$ ${
|
|
40028
|
-
$ ${
|
|
40029
|
-
$ ${
|
|
40473
|
+
$ ${CLI_NAME33} sql execute "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)"
|
|
40474
|
+
$ ${CLI_NAME33} sql execute "INSERT INTO notes (body) VALUES (?)" --params '["first note"]'
|
|
40475
|
+
$ ${CLI_NAME33} sql execute "UPDATE notes SET body = ? WHERE id = ?" --params '["edited", 1]'
|
|
40476
|
+
$ ${CLI_NAME33} sql execute "DROP TABLE old_notes" --db archive
|
|
40477
|
+
$ ${CLI_NAME33} sql execute "DELETE FROM conversation WHERE id = ?" --space applications --db xyz.tinycloud.listen/conversations --params '["abc"]'
|
|
40030
40478
|
|
|
40031
40479
|
Output:
|
|
40032
40480
|
Returns JSON with the changed row count and last inserted row id when available.
|
|
@@ -40055,9 +40503,9 @@ Output:
|
|
|
40055
40503
|
sql.command("export").description("Export a SQLite database as a binary .db file").option("--db <name>", "SQLite database name within the current space", "default").option("--space <name|uri>", "Target a non-primary space (short name or full URI)").option("-o, --output <file>", "Output file path", "export.db").addHelpText("after", `
|
|
40056
40504
|
|
|
40057
40505
|
Examples:
|
|
40058
|
-
$ ${
|
|
40059
|
-
$ ${
|
|
40060
|
-
$ ${
|
|
40506
|
+
$ ${CLI_NAME33} sql export
|
|
40507
|
+
$ ${CLI_NAME33} sql export --db analytics --output analytics.db
|
|
40508
|
+
$ ${CLI_NAME33} sql export --space applications --db xyz.tinycloud.listen/conversations --output listen.db
|
|
40061
40509
|
|
|
40062
40510
|
Output:
|
|
40063
40511
|
Writes the database file locally and returns JSON with the path and size.
|
|
@@ -40090,19 +40538,19 @@ Output:
|
|
|
40090
40538
|
sql.command("copy").description("Copy rows between SQL databases (optionally across spaces)").requiredOption("--from-db <name>", "Source database name").requiredOption("--to-db <name>", "Destination database name").option("--from-space <name|uri>", "Source space (defaults to primary)").option("--to-space <name|uri>", "Destination space (defaults to primary)").option("--table <name...>", "Restrict copy to specific tables (repeat or comma-separated)").option("--dry-run", "Print the plan without writing", false).addHelpText("after", `
|
|
40091
40539
|
|
|
40092
40540
|
Examples:
|
|
40093
|
-
$ ${
|
|
40541
|
+
$ ${CLI_NAME33} sql copy --from-db com.tinycloud.conversation-sync/conversations \\
|
|
40094
40542
|
--to-db xyz.tinycloud.listen/conversations \\
|
|
40095
40543
|
--space applications --dry-run
|
|
40096
|
-
$ ${
|
|
40544
|
+
$ ${CLI_NAME33} sql copy --from-space applications --from-db com.foo/data \\
|
|
40097
40545
|
--to-space applications --to-db com.bar/data \\
|
|
40098
40546
|
--table conversation --table participant
|
|
40099
40547
|
|
|
40100
40548
|
Notes:
|
|
40101
40549
|
- Refuses to run when (resolved space, db) is identical for source and destination.
|
|
40102
|
-
- Does NOT create destination tables. Run the target app once (or use \`${
|
|
40550
|
+
- Does NOT create destination tables. Run the target app once (or use \`${CLI_NAME33} sql execute\`)
|
|
40103
40551
|
to materialize the schema before copying.
|
|
40104
40552
|
- One row at a time; suitable for small/medium datasets. Large copies should
|
|
40105
|
-
use \`${
|
|
40553
|
+
use \`${CLI_NAME33} sql export\` + bulk import.
|
|
40106
40554
|
- Authorization: the active session/delegation must cover sql/read on source
|
|
40107
40555
|
AND sql/write on destination. Otherwise the relevant operation will fail.
|
|
40108
40556
|
`).action(async (options, cmd) => {
|
|
@@ -40202,7 +40650,7 @@ init_errors();
|
|
|
40202
40650
|
init_formatter();
|
|
40203
40651
|
init_theme();
|
|
40204
40652
|
init_permissions();
|
|
40205
|
-
import { CLI_NAME as
|
|
40653
|
+
import { CLI_NAME as CLI_NAME34 } from "@tinycloud/operations/cli-name";
|
|
40206
40654
|
import {
|
|
40207
40655
|
NodeWasmBindings as NodeWasmBindings2
|
|
40208
40656
|
} from "@tinycloud/node-sdk";
|
|
@@ -40476,7 +40924,7 @@ function formatStatus(summary) {
|
|
|
40476
40924
|
lines.push(`Default profile: ${theme.value(summary.defaultProfile)}`);
|
|
40477
40925
|
lines.push("");
|
|
40478
40926
|
if (summary.profiles.length === 0) {
|
|
40479
|
-
lines.push(theme.muted(`No profiles configured. Run: ${
|
|
40927
|
+
lines.push(theme.muted(`No profiles configured. Run: ${CLI_NAME34} init`));
|
|
40480
40928
|
return `${lines.join("\n")}
|
|
40481
40929
|
`;
|
|
40482
40930
|
}
|
|
@@ -40975,6 +41423,7 @@ function registerTinyCloudCommands(program) {
|
|
|
40975
41423
|
registerDuckdbCommand(program);
|
|
40976
41424
|
registerManifestCommand(program);
|
|
40977
41425
|
registerKnowledgeCommand(program);
|
|
41426
|
+
registerAppsCommand(program);
|
|
40978
41427
|
registerUpgradeCommand(program);
|
|
40979
41428
|
registerStatusCommand(program);
|
|
40980
41429
|
registerAccountCommand(program);
|