nimblybase 0.1.6 → 0.1.8
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 +27 -0
- package/dist/index.js +306 -152
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,27 @@ keychain on macOS, and otherwise in
|
|
|
30
30
|
`~/.config/nimblybase/credentials.json` with permissions `0600`. `nimbly
|
|
31
31
|
login` always says which one it used.
|
|
32
32
|
|
|
33
|
+
Inside an existing linked repository, `nimbly login --api <linked-api>` requests
|
|
34
|
+
access only to that project. The browser shows its authoritative name and asks
|
|
35
|
+
for explicit approval. The CLI verifies the server's restriction before saving
|
|
36
|
+
anything. Project creation, billing and other projects are excluded; your current
|
|
37
|
+
workspace role still limits the approved operations.
|
|
38
|
+
|
|
39
|
+
Each repository stores its grant separately in a keychain entry or under
|
|
40
|
+
`~/.config/nimblybase/repositories/<directory-hash>/credentials.json`. Credentials
|
|
41
|
+
stay outside your source tree; `nimblybase.json` only records the project and a
|
|
42
|
+
credential-free `authorization: "project"` marker. Nested directories use the
|
|
43
|
+
same login. `nimbly logout` here clears only this repository login and requires
|
|
44
|
+
another approval before project commands can run; your workspace login is kept.
|
|
45
|
+
|
|
46
|
+
For an unlinked existing repository, use `nimbly login --project <uuid> --api
|
|
47
|
+
<api>`. It links the approved project after verification. Legacy linked repos
|
|
48
|
+
keep their existing workspace login until they explicitly sign in this way.
|
|
49
|
+
Automatic restriction after creating a new project is a separate pending step.
|
|
50
|
+
Project login currently requires the browser flow; device sign-in and pasted
|
|
51
|
+
PAT login remain available outside linked repositories. An explicitly supplied
|
|
52
|
+
`NIMBLYBASE_TOKEN` still takes precedence for automation.
|
|
53
|
+
|
|
33
54
|
For a machine without a local browser, run `nimbly login --device`. Open the
|
|
34
55
|
displayed verification page on another device and enter the short code. The
|
|
35
56
|
dashboard shows the requesting client and approved workspace/scopes. The CLI
|
|
@@ -252,3 +273,9 @@ nimbly sql "select * from notes where id = ?" --param 7
|
|
|
252
273
|
go into the statement string: a CLI whose only option is to paste them in
|
|
253
274
|
teaches everybody — and every agent reading a shell history — to write
|
|
254
275
|
injectable SQL.
|
|
276
|
+
|
|
277
|
+
Public frontend keys can be issued after browser/device login with
|
|
278
|
+
`nimbly keys create "web frontend" --type anon` (requires `projects:write`).
|
|
279
|
+
They obey row and storage policies. `--type service`, key listing and revocation
|
|
280
|
+
require separate session/PAT key-management authority and remain unavailable
|
|
281
|
+
through OAuth. Never put the agent’s OAuth credential in application code.
|
package/dist/index.js
CHANGED
|
@@ -185,6 +185,7 @@ USAGE
|
|
|
185
185
|
|
|
186
186
|
GETTING STARTED
|
|
187
187
|
login Sign in through your browser; --device for another machine
|
|
188
|
+
Inside a linked repo, approve only that project
|
|
188
189
|
--token-only is the explicit pasted-PAT fallback
|
|
189
190
|
init [name] Create or explicitly choose a project for this repo
|
|
190
191
|
describe Show what the linked project's backend can do
|
|
@@ -373,7 +374,7 @@ function when(iso) {
|
|
|
373
374
|
|
|
374
375
|
// src/api.ts
|
|
375
376
|
var DEFAULT_API_URL = "https://api-uat.nimblybase.com";
|
|
376
|
-
var VERSION = "0.1.
|
|
377
|
+
var VERSION = "0.1.8";
|
|
377
378
|
function dashboardUrl(api) {
|
|
378
379
|
const match = /^https:\/\/api(-[a-z0-9]+)?\.(.+)$/.exec(api);
|
|
379
380
|
return match ? `https://app${match[1] ?? ""}.${match[2]}` : null;
|
|
@@ -473,8 +474,8 @@ function createClient(base, token) {
|
|
|
473
474
|
resumeProject: (id) => req(`/v1/projects/${id}/resume`, { method: "POST", body: "{}" }),
|
|
474
475
|
keys: (projectId) => req(`/v1/projects/${projectId}/keys`),
|
|
475
476
|
createKey: (projectId, input) => req(
|
|
476
|
-
`/v1/projects/${projectId}
|
|
477
|
-
{ method: "POST", body: JSON.stringify(input) }
|
|
477
|
+
`/v1/projects/${projectId}/${input.type === "anon" ? "application-keys" : "keys"}`,
|
|
478
|
+
{ method: "POST", body: JSON.stringify(input.type === "anon" ? { name: input.name } : input) }
|
|
478
479
|
),
|
|
479
480
|
revokeKey: (keyId) => req(`/v1/keys/${keyId}`, { method: "DELETE" }),
|
|
480
481
|
// --- project data ---------------------------------------------------
|
|
@@ -808,7 +809,7 @@ async function deviceLogin(input, fetchImpl = fetch) {
|
|
|
808
809
|
throw new Error("the device verification page is outside the authorization origin");
|
|
809
810
|
}
|
|
810
811
|
const now = input.now ?? (() => performance.now());
|
|
811
|
-
const sleep = input.sleep ?? ((ms) => new Promise((
|
|
812
|
+
const sleep = input.sleep ?? ((ms) => new Promise((resolve9) => setTimeout(resolve9, ms)));
|
|
812
813
|
const deadline = now() + device.expires_in * 1e3;
|
|
813
814
|
let interval = Math.max(5, device.interval ?? 5) * 1e3;
|
|
814
815
|
input.announce({ verificationUri: verification.toString(), userCode: device.user_code, expiresInSeconds: device.expires_in });
|
|
@@ -864,6 +865,7 @@ async function browserLogin(input) {
|
|
|
864
865
|
const port = await listener.port;
|
|
865
866
|
const redirectUri = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
|
|
866
867
|
const authorize = new URL(doc.authorization_endpoint);
|
|
868
|
+
if (input.projectId) authorize.searchParams.set("project_id", input.projectId);
|
|
867
869
|
for (const [k, v] of Object.entries({
|
|
868
870
|
response_type: "code",
|
|
869
871
|
client_id: clientId,
|
|
@@ -905,7 +907,7 @@ import { promisify } from "node:util";
|
|
|
905
907
|
import { homedir } from "node:os";
|
|
906
908
|
import { join, dirname } from "node:path";
|
|
907
909
|
import { mkdir, readFile, writeFile, chmod, rm, stat, rename } from "node:fs/promises";
|
|
908
|
-
import { randomUUID } from "node:crypto";
|
|
910
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
909
911
|
var run = promisify(execFile);
|
|
910
912
|
var SERVICE = "nimblybase-cli";
|
|
911
913
|
var ACCOUNT = "access-token";
|
|
@@ -914,17 +916,21 @@ function configDir() {
|
|
|
914
916
|
return xdg ? join(xdg, "nimblybase") : join(homedir(), ".config", "nimblybase");
|
|
915
917
|
}
|
|
916
918
|
var credentialsPath = () => join(configDir(), "credentials.json");
|
|
919
|
+
function repositoryCredentials(root) {
|
|
920
|
+
const id = createHash2("sha256").update(root).digest("hex");
|
|
921
|
+
return createStore("repository-" + id, () => join(configDir(), "repositories", id, "credentials.json"), true);
|
|
922
|
+
}
|
|
917
923
|
function keychainAvailable() {
|
|
918
924
|
return process.platform === "darwin" && process.env.NIMBLYBASE_NO_KEYCHAIN !== "1";
|
|
919
925
|
}
|
|
920
|
-
async function keychainSet(secret) {
|
|
926
|
+
async function keychainSet(secret, account = ACCOUNT) {
|
|
921
927
|
if (!keychainAvailable()) return false;
|
|
922
928
|
try {
|
|
923
929
|
await run("security", [
|
|
924
930
|
"add-generic-password",
|
|
925
931
|
"-U",
|
|
926
932
|
"-a",
|
|
927
|
-
|
|
933
|
+
account,
|
|
928
934
|
"-s",
|
|
929
935
|
SERVICE,
|
|
930
936
|
"-D",
|
|
@@ -937,20 +943,20 @@ async function keychainSet(secret) {
|
|
|
937
943
|
return false;
|
|
938
944
|
}
|
|
939
945
|
}
|
|
940
|
-
async function keychainGet() {
|
|
946
|
+
async function keychainGet(account = ACCOUNT) {
|
|
941
947
|
if (!keychainAvailable()) return null;
|
|
942
948
|
try {
|
|
943
|
-
const { stdout } = await run("security", ["find-generic-password", "-a",
|
|
949
|
+
const { stdout } = await run("security", ["find-generic-password", "-a", account, "-s", SERVICE, "-w"]);
|
|
944
950
|
const value = stdout.trim();
|
|
945
951
|
return value.length > 0 ? value : null;
|
|
946
952
|
} catch {
|
|
947
953
|
return null;
|
|
948
954
|
}
|
|
949
955
|
}
|
|
950
|
-
async function keychainDelete() {
|
|
956
|
+
async function keychainDelete(account = ACCOUNT) {
|
|
951
957
|
if (!keychainAvailable()) return false;
|
|
952
958
|
try {
|
|
953
|
-
await run("security", ["delete-generic-password", "-a",
|
|
959
|
+
await run("security", ["delete-generic-password", "-a", account, "-s", SERVICE]);
|
|
954
960
|
return true;
|
|
955
961
|
} catch {
|
|
956
962
|
return false;
|
|
@@ -970,15 +976,17 @@ function decodeSecret(raw) {
|
|
|
970
976
|
return {};
|
|
971
977
|
}
|
|
972
978
|
}
|
|
973
|
-
async function fileRead() {
|
|
979
|
+
async function fileRead(path = credentialsPath(), strict = false) {
|
|
974
980
|
try {
|
|
975
|
-
|
|
976
|
-
|
|
981
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
982
|
+
if (strict && (!value || typeof value !== "object")) throw new Error("Invalid repository credential");
|
|
983
|
+
return value;
|
|
984
|
+
} catch (err) {
|
|
985
|
+
if (strict && err.code !== "ENOENT") throw new Error("Repository authorization is unreadable. Run nimbly login here again; your workspace login was kept.", { cause: err });
|
|
977
986
|
return null;
|
|
978
987
|
}
|
|
979
988
|
}
|
|
980
|
-
async function fileWrite(value, apiUrl2) {
|
|
981
|
-
const path = credentialsPath();
|
|
989
|
+
async function fileWrite(value, apiUrl2, path = credentialsPath()) {
|
|
982
990
|
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
983
991
|
await chmod(dirname(path), 448);
|
|
984
992
|
const body = typeof value === "string" ? { token: value, ...apiUrl2 ? { apiUrl: apiUrl2 } : {} } : { grant: value };
|
|
@@ -992,110 +1000,67 @@ async function fileWrite(value, apiUrl2) {
|
|
|
992
1000
|
await rm(temporary, { force: true });
|
|
993
1001
|
}
|
|
994
1002
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
if (
|
|
998
|
-
|
|
999
|
-
return "file";
|
|
1000
|
-
},
|
|
1001
|
-
async saveGrant(grant) {
|
|
1002
|
-
if (await keychainSet(encodeSecret(grant))) return "keychain";
|
|
1003
|
-
await fileWrite(grant);
|
|
1004
|
-
return "file";
|
|
1005
|
-
},
|
|
1006
|
-
async load() {
|
|
1007
|
-
const fromEnv = process.env.NIMBLYBASE_TOKEN?.trim();
|
|
1008
|
-
if (fromEnv) return { token: fromEnv, where: "environment" };
|
|
1009
|
-
const fromKeychain = await keychainGet();
|
|
1010
|
-
if (fromKeychain) {
|
|
1011
|
-
const { token, grant, apiUrl: apiUrl3 } = decodeSecret(fromKeychain);
|
|
1012
|
-
const value2 = token ?? grant?.accessToken;
|
|
1013
|
-
if (value2) return { token: value2, where: "keychain", ...apiUrl3 || grant?.issuer ? { apiUrl: apiUrl3 ?? grant.issuer } : {} };
|
|
1014
|
-
}
|
|
1015
|
-
const fromFile = await fileRead();
|
|
1016
|
-
const value = fromFile?.token ?? fromFile?.grant?.accessToken;
|
|
1017
|
-
const apiUrl2 = fromFile?.apiUrl ?? fromFile?.grant?.issuer;
|
|
1018
|
-
return value ? { token: value, where: "file", ...apiUrl2 ? { apiUrl: apiUrl2 } : {} } : null;
|
|
1019
|
-
},
|
|
1020
|
-
async loadGrant() {
|
|
1021
|
-
if (process.env.NIMBLYBASE_TOKEN?.trim()) return null;
|
|
1022
|
-
const fromKeychain = await keychainGet();
|
|
1023
|
-
if (fromKeychain) {
|
|
1024
|
-
const { grant } = decodeSecret(fromKeychain);
|
|
1025
|
-
return grant ? { grant, where: "keychain" } : null;
|
|
1026
|
-
}
|
|
1027
|
-
const fromFile = await fileRead();
|
|
1028
|
-
return fromFile?.grant ? { grant: fromFile.grant, where: "file" } : null;
|
|
1029
|
-
},
|
|
1030
|
-
async clear() {
|
|
1031
|
-
const cleared = [];
|
|
1032
|
-
if (await keychainDelete()) cleared.push("keychain");
|
|
1033
|
-
try {
|
|
1034
|
-
await stat(credentialsPath());
|
|
1035
|
-
await rm(credentialsPath());
|
|
1036
|
-
cleared.push("file");
|
|
1037
|
-
} catch {
|
|
1003
|
+
function createStore(account = ACCOUNT, path = credentialsPath, projectOnly = false) {
|
|
1004
|
+
function checkProject(grant) {
|
|
1005
|
+
if (projectOnly && (!grant?.projectId || !grant.issuer || !grant.refreshToken || !grant.accessToken)) {
|
|
1006
|
+
throw new Error("Repository authorization is invalid. Run nimbly login here again; your workspace login was kept.");
|
|
1038
1007
|
}
|
|
1039
|
-
return cleared;
|
|
1040
1008
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1009
|
+
return {
|
|
1010
|
+
async save(token, apiUrl2) {
|
|
1011
|
+
if (projectOnly) throw new Error("Repository authorization requires a project-restricted OAuth grant");
|
|
1012
|
+
if (await keychainSet(encodeSecret(token, apiUrl2), account)) return "keychain";
|
|
1013
|
+
await fileWrite(token, apiUrl2, path());
|
|
1014
|
+
return "file";
|
|
1015
|
+
},
|
|
1016
|
+
async saveGrant(grant) {
|
|
1017
|
+
checkProject(grant);
|
|
1018
|
+
if (await keychainSet(encodeSecret(grant), account)) return "keychain";
|
|
1019
|
+
await fileWrite(grant, void 0, path());
|
|
1020
|
+
return "file";
|
|
1021
|
+
},
|
|
1022
|
+
async load() {
|
|
1023
|
+
const fromEnv = process.env.NIMBLYBASE_TOKEN?.trim();
|
|
1024
|
+
if (fromEnv) return { token: fromEnv, where: "environment" };
|
|
1025
|
+
const fromKeychain = await keychainGet(account);
|
|
1026
|
+
if (fromKeychain) {
|
|
1027
|
+
const { token, grant, apiUrl: apiUrl3 } = decodeSecret(fromKeychain);
|
|
1028
|
+
checkProject(grant);
|
|
1029
|
+
const value2 = token ?? grant?.accessToken;
|
|
1030
|
+
if (value2) return { token: value2, where: "keychain", ...apiUrl3 || grant?.issuer ? { apiUrl: apiUrl3 ?? grant.issuer } : {} };
|
|
1062
1031
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
;
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
const answer = await new Promise((r) => rl.question(`${question} [y/N] `, r));
|
|
1094
|
-
return /^y(es)?$/i.test(answer.trim());
|
|
1095
|
-
} finally {
|
|
1096
|
-
rl.close();
|
|
1097
|
-
}
|
|
1032
|
+
const fromFile = await fileRead(path(), projectOnly);
|
|
1033
|
+
if (fromFile) checkProject(fromFile.grant);
|
|
1034
|
+
const value = fromFile?.token ?? fromFile?.grant?.accessToken;
|
|
1035
|
+
const apiUrl2 = fromFile?.apiUrl ?? fromFile?.grant?.issuer;
|
|
1036
|
+
return value ? { token: value, where: "file", ...apiUrl2 ? { apiUrl: apiUrl2 } : {} } : null;
|
|
1037
|
+
},
|
|
1038
|
+
async loadGrant() {
|
|
1039
|
+
if (process.env.NIMBLYBASE_TOKEN?.trim()) return null;
|
|
1040
|
+
const fromKeychain = await keychainGet(account);
|
|
1041
|
+
if (fromKeychain) {
|
|
1042
|
+
const { grant } = decodeSecret(fromKeychain);
|
|
1043
|
+
checkProject(grant);
|
|
1044
|
+
return grant ? { grant, where: "keychain" } : null;
|
|
1045
|
+
}
|
|
1046
|
+
const fromFile = await fileRead(path(), projectOnly);
|
|
1047
|
+
if (fromFile) checkProject(fromFile.grant);
|
|
1048
|
+
return fromFile?.grant ? { grant: fromFile.grant, where: "file" } : null;
|
|
1049
|
+
},
|
|
1050
|
+
async clear() {
|
|
1051
|
+
const cleared = [];
|
|
1052
|
+
if (await keychainDelete(account)) cleared.push("keychain");
|
|
1053
|
+
try {
|
|
1054
|
+
await stat(path());
|
|
1055
|
+
await rm(path());
|
|
1056
|
+
cleared.push("file");
|
|
1057
|
+
} catch {
|
|
1058
|
+
}
|
|
1059
|
+
return cleared;
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1098
1062
|
}
|
|
1063
|
+
var credentials = createStore();
|
|
1099
1064
|
|
|
1100
1065
|
// src/link.ts
|
|
1101
1066
|
import { readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2, link as hardLink } from "node:fs/promises";
|
|
@@ -1146,6 +1111,7 @@ async function findLink(from = process.cwd()) {
|
|
|
1146
1111
|
projectId: parsed.projectId,
|
|
1147
1112
|
slug: typeof parsed.slug === "string" ? parsed.slug : "",
|
|
1148
1113
|
apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : void 0,
|
|
1114
|
+
...parsed.authorization === "project" ? { authorization: "project" } : {},
|
|
1149
1115
|
path
|
|
1150
1116
|
};
|
|
1151
1117
|
}
|
|
@@ -1181,6 +1147,7 @@ async function writeLink(dir, link2, options = {}) {
|
|
|
1181
1147
|
slug: link2.slug
|
|
1182
1148
|
};
|
|
1183
1149
|
if (link2.apiUrl !== void 0) next.apiUrl = link2.apiUrl;
|
|
1150
|
+
if (link2.authorization !== void 0) next.authorization = link2.authorization;
|
|
1184
1151
|
const temporary = join2(dirname2(path), `.${LINK_FILE}.${process.pid}.${randomUUID2()}.tmp`);
|
|
1185
1152
|
try {
|
|
1186
1153
|
await writeFile2(temporary, `${JSON.stringify(next, null, 2)}
|
|
@@ -1236,12 +1203,86 @@ async function clearPendingBootstrap(dir) {
|
|
|
1236
1203
|
await rm2(join2(dir, BOOTSTRAP_FILE), { force: true });
|
|
1237
1204
|
}
|
|
1238
1205
|
|
|
1206
|
+
// src/commands/auth.ts
|
|
1207
|
+
import { realpath as realpath2 } from "node:fs/promises";
|
|
1208
|
+
import { dirname as dirname4, resolve as resolve2 } from "node:path";
|
|
1209
|
+
|
|
1210
|
+
// src/prompt.ts
|
|
1211
|
+
import { createInterface } from "node:readline";
|
|
1212
|
+
async function readPipedInput() {
|
|
1213
|
+
if (process.stdin.isTTY) return null;
|
|
1214
|
+
const chunks = [];
|
|
1215
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
1216
|
+
const value = Buffer.concat(chunks).toString("utf8").trim();
|
|
1217
|
+
return value.length > 0 ? value : null;
|
|
1218
|
+
}
|
|
1219
|
+
function promptSecret(question) {
|
|
1220
|
+
return new Promise((resolve9, reject) => {
|
|
1221
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
1222
|
+
let promptShown = false;
|
|
1223
|
+
const output = rl;
|
|
1224
|
+
const write = output.output.write.bind(output.output);
|
|
1225
|
+
output.output.write = (chunk) => {
|
|
1226
|
+
if (!promptShown) {
|
|
1227
|
+
promptShown = true;
|
|
1228
|
+
return write(chunk);
|
|
1229
|
+
}
|
|
1230
|
+
return true;
|
|
1231
|
+
};
|
|
1232
|
+
rl.question(question, (answer) => {
|
|
1233
|
+
;
|
|
1234
|
+
output.output.write = write;
|
|
1235
|
+
process.stderr.write("\n");
|
|
1236
|
+
rl.close();
|
|
1237
|
+
resolve9(answer.trim());
|
|
1238
|
+
});
|
|
1239
|
+
rl.on("SIGINT", () => {
|
|
1240
|
+
rl.close();
|
|
1241
|
+
reject(new Error("cancelled"));
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
async function confirmExact(question, expected) {
|
|
1246
|
+
if (!process.stdin.isTTY) return false;
|
|
1247
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1248
|
+
try {
|
|
1249
|
+
const answer = await new Promise((r) => rl.question(`${question}
|
|
1250
|
+
Type "${expected}" to confirm: `, r));
|
|
1251
|
+
return answer.trim() === expected;
|
|
1252
|
+
} finally {
|
|
1253
|
+
rl.close();
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
async function confirm(question) {
|
|
1257
|
+
if (!process.stdin.isTTY) return false;
|
|
1258
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1259
|
+
try {
|
|
1260
|
+
const answer = await new Promise((r) => rl.question(`${question} [y/N] `, r));
|
|
1261
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1262
|
+
} finally {
|
|
1263
|
+
rl.close();
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1239
1267
|
// src/context.ts
|
|
1268
|
+
import { realpath } from "node:fs/promises";
|
|
1269
|
+
import { dirname as dirname3 } from "node:path";
|
|
1240
1270
|
async function context(apiOverride) {
|
|
1241
1271
|
const link2 = await findLink();
|
|
1242
|
-
const
|
|
1272
|
+
const repositoryStore = link2 ? repositoryCredentials(await realpath(dirname3(link2.path))) : void 0;
|
|
1273
|
+
const repositoryToken = await repositoryStore?.load();
|
|
1274
|
+
const useRepository = repositoryToken?.where !== "environment" && (Boolean(repositoryToken) || link2?.authorization === "project");
|
|
1275
|
+
const store = useRepository ? repositoryStore : credentials;
|
|
1276
|
+
const storedBeforeRefresh = useRepository ? repositoryToken : await credentials.load();
|
|
1243
1277
|
const base = apiUrl(apiOverride ?? process.env.NIMBLYBASE_API_URL ?? link2?.apiUrl ?? (storedBeforeRefresh?.where !== "environment" ? storedBeforeRefresh?.apiUrl : void 0));
|
|
1244
|
-
const held = await
|
|
1278
|
+
const held = await store.loadGrant();
|
|
1279
|
+
if (useRepository && held && held.grant.projectId !== link2?.projectId) {
|
|
1280
|
+
throw new CliError(
|
|
1281
|
+
"This repository login belongs to a different project.",
|
|
1282
|
+
EXIT.unauthenticated,
|
|
1283
|
+
"Run `nimbly login` here to approve the new project. Your other logins were kept."
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1245
1286
|
if (storedBeforeRefresh && storedBeforeRefresh.where !== "environment") {
|
|
1246
1287
|
const bound = apiUrl(storedBeforeRefresh.apiUrl ?? "https://api-uat.nimblybase.com");
|
|
1247
1288
|
if (base !== bound) throw new CliError(
|
|
@@ -1264,11 +1305,11 @@ async function context(apiOverride) {
|
|
|
1264
1305
|
clientId: held.grant.clientId,
|
|
1265
1306
|
refreshToken: held.grant.refreshToken
|
|
1266
1307
|
});
|
|
1267
|
-
await
|
|
1308
|
+
await store.saveGrant({ ...fresh, issuer: held.grant.issuer, ...held.grant.projectId ? { projectId: held.grant.projectId } : {} });
|
|
1268
1309
|
} catch {
|
|
1269
1310
|
}
|
|
1270
1311
|
}
|
|
1271
|
-
const stored = await
|
|
1312
|
+
const stored = await store.load();
|
|
1272
1313
|
if (!stored) {
|
|
1273
1314
|
throw new CliError(
|
|
1274
1315
|
"Not signed in.",
|
|
@@ -1281,7 +1322,8 @@ async function context(apiOverride) {
|
|
|
1281
1322
|
base,
|
|
1282
1323
|
tokenFrom: stored.where,
|
|
1283
1324
|
isCurrentToken: (prefix) => prefix.length > 0 && stored.token.startsWith(prefix),
|
|
1284
|
-
link: link2
|
|
1325
|
+
link: link2,
|
|
1326
|
+
credentialScope: useRepository ? "repository" : "workspace"
|
|
1285
1327
|
};
|
|
1286
1328
|
}
|
|
1287
1329
|
var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -1322,7 +1364,34 @@ var WHERE_SAID = {
|
|
|
1322
1364
|
};
|
|
1323
1365
|
async function login(flags) {
|
|
1324
1366
|
const base = apiUrl(flags.api);
|
|
1367
|
+
const link2 = await findLink(flags.dir);
|
|
1368
|
+
const projectId = flags.project ?? link2?.projectId;
|
|
1369
|
+
if (projectId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(projectId)) {
|
|
1370
|
+
throw new CliError("Project authorization requires a project UUID.", EXIT.usage, "Link this repository first, or pass --project <uuid>.");
|
|
1371
|
+
}
|
|
1372
|
+
if (projectId && flags.device) throw new CliError(
|
|
1373
|
+
"Project sign-in currently requires browser approval.",
|
|
1374
|
+
EXIT.usage,
|
|
1375
|
+
"Run `nimbly login` here without --device. Device sign-in outside linked repositories remains workspace-scoped."
|
|
1376
|
+
);
|
|
1377
|
+
if (projectId && link2?.apiUrl && apiUrl(link2.apiUrl) !== base) throw new CliError(
|
|
1378
|
+
"Choose the linked project API explicitly.",
|
|
1379
|
+
EXIT.usage,
|
|
1380
|
+
"Pass --api with the linked API; a repository file cannot choose your login destination."
|
|
1381
|
+
);
|
|
1382
|
+
if (link2 && projectId !== link2.projectId && !flags.relink) throw new CliError(
|
|
1383
|
+
"This repository is linked to another project.",
|
|
1384
|
+
EXIT.usage,
|
|
1385
|
+
"Use --relink to explicitly authorize and link a different project."
|
|
1386
|
+
);
|
|
1387
|
+
const root = projectId ? await realpath2(link2 ? dirname4(link2.path) : resolve2(flags.dir ?? ".")) : void 0;
|
|
1388
|
+
const store = root ? repositoryCredentials(root) : credentials;
|
|
1325
1389
|
const given = flags.token ?? await readPipedInput();
|
|
1390
|
+
if (projectId && (given || flags.tokenOnly)) throw new CliError(
|
|
1391
|
+
"A pasted token does not authorize a repository project.",
|
|
1392
|
+
EXIT.usage,
|
|
1393
|
+
"Use browser sign-in here; use NIMBLYBASE_TOKEN for an explicitly configured automation identity."
|
|
1394
|
+
);
|
|
1326
1395
|
if (flags.device && (given || flags.tokenOnly)) throw new CliError("Choose device sign-in or a pasted token, not both.", EXIT.usage);
|
|
1327
1396
|
if (!given && !flags.tokenOnly) {
|
|
1328
1397
|
const grant = flags.device ? await deviceLogin({
|
|
@@ -1340,17 +1409,40 @@ It expires in ${expiresInSeconds} seconds. Approve only the CLI request you star
|
|
|
1340
1409
|
note(`If it did not open, go to:
|
|
1341
1410
|
${url}`);
|
|
1342
1411
|
},
|
|
1343
|
-
open: openBrowser
|
|
1412
|
+
open: openBrowser,
|
|
1413
|
+
...projectId ? { projectId } : {}
|
|
1344
1414
|
});
|
|
1345
|
-
const
|
|
1346
|
-
const
|
|
1415
|
+
const client = createClient(base, grant.accessToken);
|
|
1416
|
+
const me2 = await client.me();
|
|
1417
|
+
if (projectId && (me2.authorization?.via !== "oauth" || me2.authorization.projectId !== projectId)) {
|
|
1418
|
+
throw new CliError(
|
|
1419
|
+
"The server did not confirm this project restriction.",
|
|
1420
|
+
EXIT.unauthenticated,
|
|
1421
|
+
"Nothing was saved. Update the server/client and approve project access again."
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
if (projectId && root) {
|
|
1425
|
+
const { project } = await client.project(projectId);
|
|
1426
|
+
if (project.id !== projectId) throw new CliError("The authorized project response is invalid.", EXIT.unauthenticated);
|
|
1427
|
+
await writeLink(root, { projectId, slug: project.slug, apiUrl: base, authorization: "project" }, { relink: flags.relink });
|
|
1428
|
+
}
|
|
1429
|
+
const where2 = await store.saveGrant({ ...grant, issuer: base, ...projectId ? { projectId } : {} });
|
|
1347
1430
|
if (isJsonMode()) {
|
|
1348
|
-
json({
|
|
1431
|
+
json({
|
|
1432
|
+
ok: true,
|
|
1433
|
+
storedIn: where2,
|
|
1434
|
+
organization: me2.organization,
|
|
1435
|
+
api: base,
|
|
1436
|
+
scopes: grant.scopes,
|
|
1437
|
+
authorizationScope: projectId ? "repository" : "workspace",
|
|
1438
|
+
...projectId ? { projectId } : {}
|
|
1439
|
+
});
|
|
1349
1440
|
return;
|
|
1350
1441
|
}
|
|
1351
1442
|
out(`${paint("green", "\u2713")} Signed in as ${paint("bold", me2.organization?.name ?? "your account")}${me2.organization ? ` (${me2.organization.role})` : ""}`);
|
|
1352
1443
|
note(`Granted: ${grant.scopes.join(", ") || "no scopes"}.`);
|
|
1353
|
-
note(`
|
|
1444
|
+
note(projectId ? `Access is restricted to ${projectId}. Stored separately for this repository ${where2 === "keychain" ? "in your login keychain" : "outside the repository (permissions 0600)"}.` : `Stored in ${WHERE_SAID[where2]}.`);
|
|
1445
|
+
note("It refreshes itself; run `nimbly logout` here to clear this login.");
|
|
1354
1446
|
return;
|
|
1355
1447
|
}
|
|
1356
1448
|
const token = given ?? await promptSecret(source(base));
|
|
@@ -1374,14 +1466,18 @@ It expires in ${expiresInSeconds} seconds. Approve only the CLI request you star
|
|
|
1374
1466
|
note("The keychain was not available, so a file was used instead.");
|
|
1375
1467
|
}
|
|
1376
1468
|
}
|
|
1377
|
-
async function logout() {
|
|
1378
|
-
const
|
|
1469
|
+
async function logout(flags) {
|
|
1470
|
+
const link2 = await findLink(flags?.dir);
|
|
1471
|
+
const repo = link2 ? repositoryCredentials(await realpath2(dirname4(link2.path))) : void 0;
|
|
1472
|
+
const repository = link2?.authorization === "project" || repo && await repo.loadGrant() !== null;
|
|
1473
|
+
const cleared = await (repository ? repo : credentials).clear();
|
|
1379
1474
|
if (isJsonMode()) {
|
|
1380
1475
|
json({ ok: true, cleared });
|
|
1381
1476
|
return;
|
|
1382
1477
|
}
|
|
1383
1478
|
if (cleared.length === 0) out("Nothing stored - already signed out.");
|
|
1384
1479
|
else out(`${paint("green", "\u2713")} Signed out (cleared: ${cleared.join(", ")}).`);
|
|
1480
|
+
if (repository) note("Cleared this repository login. Your separate workspace login was kept; run logout outside this repository to clear it.");
|
|
1385
1481
|
if (process.env.NIMBLYBASE_TOKEN) {
|
|
1386
1482
|
note("NIMBLYBASE_TOKEN is still set in this shell and takes precedence - unset it too.");
|
|
1387
1483
|
}
|
|
@@ -1390,7 +1486,7 @@ async function whoami(flags) {
|
|
|
1390
1486
|
const ctx = await context(flags.api);
|
|
1391
1487
|
const me = await ctx.client.me();
|
|
1392
1488
|
if (isJsonMode()) {
|
|
1393
|
-
json({ ...me, api: ctx.base, tokenFrom: ctx.tokenFrom, link: ctx.link });
|
|
1489
|
+
json({ ...me, api: ctx.base, tokenFrom: ctx.tokenFrom, link: ctx.link, credentialScope: ctx.credentialScope });
|
|
1394
1490
|
return;
|
|
1395
1491
|
}
|
|
1396
1492
|
table(["", ""], [
|
|
@@ -1399,6 +1495,7 @@ async function whoami(flags) {
|
|
|
1399
1495
|
["role", me.organization?.role ?? "-"],
|
|
1400
1496
|
["api", ctx.base],
|
|
1401
1497
|
["token from", ctx.tokenFrom],
|
|
1498
|
+
["credential scope", ctx.credentialScope],
|
|
1402
1499
|
["linked project", ctx.link ? `${ctx.link.slug || ctx.link.projectId} \xB7 ${ctx.link.path}` : "none"]
|
|
1403
1500
|
]);
|
|
1404
1501
|
out();
|
|
@@ -1412,8 +1509,8 @@ var AFTER_CREATE = [
|
|
|
1412
1509
|
];
|
|
1413
1510
|
|
|
1414
1511
|
// src/creation-plan.ts
|
|
1415
|
-
import { createHash as
|
|
1416
|
-
import { resolve as
|
|
1512
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1513
|
+
import { resolve as resolve4 } from "node:path";
|
|
1417
1514
|
|
|
1418
1515
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
1419
1516
|
var external_exports = {};
|
|
@@ -5585,6 +5682,7 @@ var PendingInvitation = external_exports.object({
|
|
|
5585
5682
|
expiresAt: IsoDateTime
|
|
5586
5683
|
});
|
|
5587
5684
|
var Me = external_exports.object({
|
|
5685
|
+
authorization: external_exports.object({ via: external_exports.enum(["session", "token", "oauth"]), projectId: external_exports.string().uuid().nullable() }).optional(),
|
|
5588
5686
|
user: external_exports.object({ id: Uuid, clerkUserId: external_exports.string() }),
|
|
5589
5687
|
organization: external_exports.object({ id: Uuid, name: external_exports.string(), slug: external_exports.string(), role: Role }).nullable(),
|
|
5590
5688
|
permissions: external_exports.array(external_exports.string())
|
|
@@ -5665,6 +5763,9 @@ var BootstrapBody = external_exports.object({
|
|
|
5665
5763
|
}).refine((body) => !(body.planId || body.factsFingerprint) || Boolean(body.name), {
|
|
5666
5764
|
message: "planId and factsFingerprint are only valid when creating a named project"
|
|
5667
5765
|
});
|
|
5766
|
+
var CreateApplicationKeyBody = external_exports.object({
|
|
5767
|
+
name: external_exports.string().min(1).max(100)
|
|
5768
|
+
}).strict();
|
|
5668
5769
|
var CreateApiKeyBody = external_exports.object({
|
|
5669
5770
|
name: external_exports.string().min(1).max(100),
|
|
5670
5771
|
type: ApiKeyType
|
|
@@ -5866,6 +5967,20 @@ var PendingInvitationListResponse = external_exports.object({ invitations: exter
|
|
|
5866
5967
|
var RedeemedInvitationResponse = external_exports.object({
|
|
5867
5968
|
organization: external_exports.object({ id: Uuid, role: Role })
|
|
5868
5969
|
});
|
|
5970
|
+
var OAuthConnection = external_exports.object({
|
|
5971
|
+
id: external_exports.string().uuid(),
|
|
5972
|
+
clientId: external_exports.string().uuid(),
|
|
5973
|
+
clientName: external_exports.string(),
|
|
5974
|
+
orgId: external_exports.string().uuid(),
|
|
5975
|
+
projectId: external_exports.string().uuid().nullable(),
|
|
5976
|
+
projectName: external_exports.string().nullable(),
|
|
5977
|
+
scopes: external_exports.array(external_exports.string()),
|
|
5978
|
+
status: external_exports.enum(["ready", "awaiting_exchange"]),
|
|
5979
|
+
connectedAt: external_exports.string().datetime(),
|
|
5980
|
+
expiresAt: external_exports.string().datetime(),
|
|
5981
|
+
lastUsedAt: external_exports.string().datetime().nullable()
|
|
5982
|
+
});
|
|
5983
|
+
var OAuthConnectionListResponse = external_exports.object({ enabled: external_exports.boolean(), connections: external_exports.array(OAuthConnection) });
|
|
5869
5984
|
|
|
5870
5985
|
// ../../node_modules/.pnpm/zod-to-json-schema@3.25.2_zod@3.25.76/node_modules/zod-to-json-schema/dist/esm/Options.js
|
|
5871
5986
|
var ignoreOverride = Symbol("Let zodToJsonSchema decide on which parser to use");
|
|
@@ -6053,6 +6168,34 @@ var routes = {
|
|
|
6053
6168
|
// All three are sessionOnly: a token must not be able to mint another
|
|
6054
6169
|
// token. Otherwise a leaked one manufactures replacements faster than
|
|
6055
6170
|
// anyone can revoke them, and revoking the one you found achieves nothing.
|
|
6171
|
+
listOAuthConnections: {
|
|
6172
|
+
method: "GET",
|
|
6173
|
+
path: "/v1/me/connections",
|
|
6174
|
+
summary: "Your authorized app connections in this workspace. No credentials.",
|
|
6175
|
+
permission: "org:read",
|
|
6176
|
+
sessionOnly: true,
|
|
6177
|
+
status: 200,
|
|
6178
|
+
response: OAuthConnectionListResponse
|
|
6179
|
+
},
|
|
6180
|
+
narrowOAuthConnection: {
|
|
6181
|
+
method: "POST",
|
|
6182
|
+
path: "/v1/me/connections/{id}/project",
|
|
6183
|
+
summary: "Permanently restrict one authorization to an existing project. Cannot widen or retarget.",
|
|
6184
|
+
permission: "org:read",
|
|
6185
|
+
sessionOnly: true,
|
|
6186
|
+
params: Id,
|
|
6187
|
+
body: external_exports.object({ projectId: external_exports.string().uuid() }).strict(),
|
|
6188
|
+
status: 204
|
|
6189
|
+
},
|
|
6190
|
+
revokeOAuthConnection: {
|
|
6191
|
+
method: "DELETE",
|
|
6192
|
+
path: "/v1/me/connections/{id}",
|
|
6193
|
+
summary: "Disconnect one consent family, including access, refresh and pending exchange.",
|
|
6194
|
+
permission: "org:read",
|
|
6195
|
+
sessionOnly: true,
|
|
6196
|
+
params: Id,
|
|
6197
|
+
status: 204
|
|
6198
|
+
},
|
|
6056
6199
|
listAccessTokens: {
|
|
6057
6200
|
method: "GET",
|
|
6058
6201
|
path: "/v1/me/tokens",
|
|
@@ -6216,6 +6359,16 @@ var routes = {
|
|
|
6216
6359
|
status: 200,
|
|
6217
6360
|
response: ApiKeyListResponse
|
|
6218
6361
|
},
|
|
6362
|
+
createApplicationKey: {
|
|
6363
|
+
method: "POST",
|
|
6364
|
+
path: "/v1/projects/{id}/application-keys",
|
|
6365
|
+
summary: "Issue a public anon application key. It obeys row and storage policies; never issues a service key.",
|
|
6366
|
+
permission: "project:update",
|
|
6367
|
+
params: Id,
|
|
6368
|
+
body: CreateApplicationKeyBody,
|
|
6369
|
+
status: 201,
|
|
6370
|
+
response: IssuedApiKeyResponse
|
|
6371
|
+
},
|
|
6219
6372
|
createProjectKey: {
|
|
6220
6373
|
method: "POST",
|
|
6221
6374
|
path: "/v1/projects/{id}/keys",
|
|
@@ -6236,8 +6389,9 @@ var routes = {
|
|
|
6236
6389
|
// The platform token never leaves the control plane and expires after 60
|
|
6237
6390
|
// seconds. Callers are therefore authorized for the operation they asked
|
|
6238
6391
|
// for (`data:read`, `data:write`, `deploy`, or `logs:read`) rather than for
|
|
6239
|
-
// the internal credential used to carry it out.
|
|
6240
|
-
//
|
|
6392
|
+
// the internal credential used to carry it out. Service-key issuance, key
|
|
6393
|
+
// listing and revocation remain under apikey:* and closed to OAuth. The
|
|
6394
|
+
// separate anon-only application-key route above uses project:update.
|
|
6241
6395
|
getProjectSchema: {
|
|
6242
6396
|
method: "GET",
|
|
6243
6397
|
path: "/v1/projects/{id}/schema",
|
|
@@ -7258,7 +7412,7 @@ function staticSite(entries) {
|
|
|
7258
7412
|
|
|
7259
7413
|
// src/commands/assess.ts
|
|
7260
7414
|
import { stat as stat3 } from "node:fs/promises";
|
|
7261
|
-
import { basename, resolve as
|
|
7415
|
+
import { basename, resolve as resolve3 } from "node:path";
|
|
7262
7416
|
|
|
7263
7417
|
// src/repository-scan.ts
|
|
7264
7418
|
import { open, readdir, stat as stat2 } from "node:fs/promises";
|
|
@@ -7497,7 +7651,7 @@ async function assessWithContext(ctx, flags, requirements = "") {
|
|
|
7497
7651
|
return;
|
|
7498
7652
|
}
|
|
7499
7653
|
if (flags.savePlan && !flags.name) throw new CliError("--save-plan requires --name with an explicit project name.", EXIT.usage);
|
|
7500
|
-
const facts = await inspectAssessmentFacts(
|
|
7654
|
+
const facts = await inspectAssessmentFacts(resolve3(flags.dir ?? "."), flags, requirements);
|
|
7501
7655
|
if (flags.savePlan) {
|
|
7502
7656
|
const result2 = ProjectPlanResponse.safeParse(await ctx.client.createProjectPlan({ name: flags.name, ...flags.slug ? { slug: flags.slug } : {}, facts }));
|
|
7503
7657
|
if (!result2.success) throw new CliError("The API returned an invalid project plan.", EXIT.failed);
|
|
@@ -7528,7 +7682,7 @@ async function assess(flags, ...words) {
|
|
|
7528
7682
|
|
|
7529
7683
|
// src/creation-plan.ts
|
|
7530
7684
|
async function prepareCreation(ctx, flags, name, operation) {
|
|
7531
|
-
const root =
|
|
7685
|
+
const root = resolve4(flags.dir ?? ".");
|
|
7532
7686
|
const me = await ctx.client.me();
|
|
7533
7687
|
if (!me.organization) throw new CliError("Select a workspace before creating a project.", EXIT.forbidden);
|
|
7534
7688
|
const previous = await readPendingBootstrap(root);
|
|
@@ -7552,7 +7706,7 @@ async function prepareCreation(ctx, flags, name, operation) {
|
|
|
7552
7706
|
EXIT.usage,
|
|
7553
7707
|
"Run nimbly assess to clarify the requirements. An empty new application needs the explicit --recipe static-d1 choice."
|
|
7554
7708
|
);
|
|
7555
|
-
const fingerprint =
|
|
7709
|
+
const fingerprint = createHash3("sha256").update(JSON.stringify(SuitabilityFacts.parse(facts))).digest("hex");
|
|
7556
7710
|
if (previous) {
|
|
7557
7711
|
if (flags.plan && flags.plan !== previous.planId) throw new CliError("Retry with the original pending plan.", EXIT.usage);
|
|
7558
7712
|
if (fingerprint !== previous.factsFingerprint) throw new CliError(
|
|
@@ -7584,7 +7738,7 @@ async function prepareCreation(ctx, flags, name, operation) {
|
|
|
7584
7738
|
}
|
|
7585
7739
|
|
|
7586
7740
|
// src/commands/projects.ts
|
|
7587
|
-
import { resolve as
|
|
7741
|
+
import { resolve as resolve5 } from "node:path";
|
|
7588
7742
|
var WAIT_TIMEOUT_MS = 18e4;
|
|
7589
7743
|
var FIRST_POLL_MS = 500;
|
|
7590
7744
|
var POLL_GROWTH = 1.5;
|
|
@@ -7642,7 +7796,7 @@ async function create(flags, name) {
|
|
|
7642
7796
|
);
|
|
7643
7797
|
}
|
|
7644
7798
|
const ctx = await context(flags.api);
|
|
7645
|
-
const root =
|
|
7799
|
+
const root = resolve5(flags.dir ?? ".");
|
|
7646
7800
|
if (flags.link !== false) {
|
|
7647
7801
|
await assertMayCreateLink(root, { relink: flags.relink });
|
|
7648
7802
|
}
|
|
@@ -7694,7 +7848,7 @@ async function init(flags, name) {
|
|
|
7694
7848
|
);
|
|
7695
7849
|
}
|
|
7696
7850
|
const ctx = await context(flags.api);
|
|
7697
|
-
const root =
|
|
7851
|
+
const root = resolve5(flags.dir ?? ".");
|
|
7698
7852
|
const explicitProject = flags.project;
|
|
7699
7853
|
const linkedProject = !name && !explicitProject ? (await findLink(root))?.projectId : void 0;
|
|
7700
7854
|
if (name && flags.link !== false) {
|
|
@@ -7968,11 +8122,11 @@ async function mcp(flags, which) {
|
|
|
7968
8122
|
// src/commands/db.ts
|
|
7969
8123
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
7970
8124
|
import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
7971
|
-
import { dirname as
|
|
8125
|
+
import { dirname as dirname5, isAbsolute, join as join4, resolve as resolve6 } from "node:path";
|
|
7972
8126
|
var DEFAULT_SCHEMA_FILE = "schema.json";
|
|
7973
8127
|
function schemaPath(ctx, flag) {
|
|
7974
|
-
if (flag) return isAbsolute(flag) ? flag :
|
|
7975
|
-
const base = ctx.link ?
|
|
8128
|
+
if (flag) return isAbsolute(flag) ? flag : resolve6(process.cwd(), flag);
|
|
8129
|
+
const base = ctx.link ? dirname5(ctx.link.path) : process.cwd();
|
|
7976
8130
|
return join4(base, DEFAULT_SCHEMA_FILE);
|
|
7977
8131
|
}
|
|
7978
8132
|
async function readSchema(path) {
|
|
@@ -8482,7 +8636,7 @@ function render4(f) {
|
|
|
8482
8636
|
|
|
8483
8637
|
// src/commands/functions.ts
|
|
8484
8638
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
8485
|
-
import { basename as basename2, dirname as
|
|
8639
|
+
import { basename as basename2, dirname as dirname6, extname as extname2, resolve as resolve7 } from "node:path";
|
|
8486
8640
|
import { build as bundle } from "esbuild";
|
|
8487
8641
|
var NAME = /^[a-z][a-z0-9-]{0,30}$/;
|
|
8488
8642
|
var MAX_FUNCTION_BYTES = 1e6;
|
|
@@ -8563,10 +8717,10 @@ async function deploy(flags, file) {
|
|
|
8563
8717
|
note(paint("dim", " env.DB, env.FILES, env.KV are this project\u2019s own. Nothing else is reachable."));
|
|
8564
8718
|
}
|
|
8565
8719
|
async function bundleFunction(file) {
|
|
8566
|
-
const absolute =
|
|
8720
|
+
const absolute = resolve7(file);
|
|
8567
8721
|
try {
|
|
8568
8722
|
const result = await bundle({
|
|
8569
|
-
absWorkingDir:
|
|
8723
|
+
absWorkingDir: dirname6(absolute),
|
|
8570
8724
|
entryPoints: [absolute],
|
|
8571
8725
|
bundle: true,
|
|
8572
8726
|
write: false,
|
|
@@ -9010,7 +9164,7 @@ function propertyName(name) {
|
|
|
9010
9164
|
|
|
9011
9165
|
// src/commands/deploy.ts
|
|
9012
9166
|
import { readdir as readdir2, readFile as readFile6, stat as stat4 } from "node:fs/promises";
|
|
9013
|
-
import { createHash as
|
|
9167
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
9014
9168
|
import { spawn as spawn2 } from "node:child_process";
|
|
9015
9169
|
import { join as join5, relative as relative2, sep } from "node:path";
|
|
9016
9170
|
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", ".env", ".env.local", "Thumbs.db"]);
|
|
@@ -9181,7 +9335,7 @@ async function buildSite(root, script, display) {
|
|
|
9181
9335
|
const entries = await readdir2(root);
|
|
9182
9336
|
const runner = packageRunner(entries);
|
|
9183
9337
|
if (!isJsonMode()) note(`Running ${runner.label} run ${script}\u2026`);
|
|
9184
|
-
const code = await new Promise((
|
|
9338
|
+
const code = await new Promise((resolve9, reject) => {
|
|
9185
9339
|
const child = spawn2(runner.command, [...runner.args, "run", script], {
|
|
9186
9340
|
cwd: root,
|
|
9187
9341
|
stdio: "inherit",
|
|
@@ -9191,7 +9345,7 @@ async function buildSite(root, script, display) {
|
|
|
9191
9345
|
shell: process.platform === "win32"
|
|
9192
9346
|
});
|
|
9193
9347
|
child.once("error", reject);
|
|
9194
|
-
child.once("close", (status4) =>
|
|
9348
|
+
child.once("close", (status4) => resolve9(status4 ?? 1));
|
|
9195
9349
|
}).catch((err) => {
|
|
9196
9350
|
throw new CliError(
|
|
9197
9351
|
`Could not start ${display ?? `${runner.label} run ${script}`}.`,
|
|
@@ -9251,7 +9405,7 @@ async function collect(dir) {
|
|
|
9251
9405
|
found.push({
|
|
9252
9406
|
path: toUrlPath(relative2(dir, absolute)),
|
|
9253
9407
|
absolute,
|
|
9254
|
-
hash:
|
|
9408
|
+
hash: createHash4("sha256").update(await readFile6(absolute)).digest("hex"),
|
|
9255
9409
|
size: info.size
|
|
9256
9410
|
});
|
|
9257
9411
|
}
|
|
@@ -9721,7 +9875,7 @@ async function remove4(flags, name) {
|
|
|
9721
9875
|
|
|
9722
9876
|
// src/commands/architect.ts
|
|
9723
9877
|
import { readdir as readdir3, readFile as readFile8, stat as stat5 } from "node:fs/promises";
|
|
9724
|
-
import { join as join6, resolve as
|
|
9878
|
+
import { join as join6, resolve as resolve8 } from "node:path";
|
|
9725
9879
|
var FEATURE_PATTERNS = {
|
|
9726
9880
|
auth: /\b(auth(?:entication|enticated|orization|orized)?|log-?ins?|sign-?ins?|sign-?ups?|sessions?|user accounts?|passport|clerk|auth0)\b/i,
|
|
9727
9881
|
storage: /\b(uploads?|attachments?|buckets?|object storage|file (?:uploads?|attachments?|storage)|s3|r2|multer|media)\b/i,
|
|
@@ -9735,7 +9889,7 @@ var FEATURE_PATTERNS = {
|
|
|
9735
9889
|
crawler: /\b(crawls?|crawlers?|scrapes?|playwright|puppeteer|browser automation)\b/i
|
|
9736
9890
|
};
|
|
9737
9891
|
async function architect(flags, ...words) {
|
|
9738
|
-
const root =
|
|
9892
|
+
const root = resolve8(flags.dir ?? ".");
|
|
9739
9893
|
const requirements = words.filter((word) => Boolean(word)).join(" ").trim();
|
|
9740
9894
|
const ctx = await context(flags.api);
|
|
9741
9895
|
if (!flags.project && !ctx.link) {
|