nimblybase 0.1.7 → 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 +21 -0
- package/dist/index.js +245 -148
- 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
|
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;
|
|
@@ -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 = {};
|
|
@@ -7315,7 +7412,7 @@ function staticSite(entries) {
|
|
|
7315
7412
|
|
|
7316
7413
|
// src/commands/assess.ts
|
|
7317
7414
|
import { stat as stat3 } from "node:fs/promises";
|
|
7318
|
-
import { basename, resolve as
|
|
7415
|
+
import { basename, resolve as resolve3 } from "node:path";
|
|
7319
7416
|
|
|
7320
7417
|
// src/repository-scan.ts
|
|
7321
7418
|
import { open, readdir, stat as stat2 } from "node:fs/promises";
|
|
@@ -7554,7 +7651,7 @@ async function assessWithContext(ctx, flags, requirements = "") {
|
|
|
7554
7651
|
return;
|
|
7555
7652
|
}
|
|
7556
7653
|
if (flags.savePlan && !flags.name) throw new CliError("--save-plan requires --name with an explicit project name.", EXIT.usage);
|
|
7557
|
-
const facts = await inspectAssessmentFacts(
|
|
7654
|
+
const facts = await inspectAssessmentFacts(resolve3(flags.dir ?? "."), flags, requirements);
|
|
7558
7655
|
if (flags.savePlan) {
|
|
7559
7656
|
const result2 = ProjectPlanResponse.safeParse(await ctx.client.createProjectPlan({ name: flags.name, ...flags.slug ? { slug: flags.slug } : {}, facts }));
|
|
7560
7657
|
if (!result2.success) throw new CliError("The API returned an invalid project plan.", EXIT.failed);
|
|
@@ -7585,7 +7682,7 @@ async function assess(flags, ...words) {
|
|
|
7585
7682
|
|
|
7586
7683
|
// src/creation-plan.ts
|
|
7587
7684
|
async function prepareCreation(ctx, flags, name, operation) {
|
|
7588
|
-
const root =
|
|
7685
|
+
const root = resolve4(flags.dir ?? ".");
|
|
7589
7686
|
const me = await ctx.client.me();
|
|
7590
7687
|
if (!me.organization) throw new CliError("Select a workspace before creating a project.", EXIT.forbidden);
|
|
7591
7688
|
const previous = await readPendingBootstrap(root);
|
|
@@ -7609,7 +7706,7 @@ async function prepareCreation(ctx, flags, name, operation) {
|
|
|
7609
7706
|
EXIT.usage,
|
|
7610
7707
|
"Run nimbly assess to clarify the requirements. An empty new application needs the explicit --recipe static-d1 choice."
|
|
7611
7708
|
);
|
|
7612
|
-
const fingerprint =
|
|
7709
|
+
const fingerprint = createHash3("sha256").update(JSON.stringify(SuitabilityFacts.parse(facts))).digest("hex");
|
|
7613
7710
|
if (previous) {
|
|
7614
7711
|
if (flags.plan && flags.plan !== previous.planId) throw new CliError("Retry with the original pending plan.", EXIT.usage);
|
|
7615
7712
|
if (fingerprint !== previous.factsFingerprint) throw new CliError(
|
|
@@ -7641,7 +7738,7 @@ async function prepareCreation(ctx, flags, name, operation) {
|
|
|
7641
7738
|
}
|
|
7642
7739
|
|
|
7643
7740
|
// src/commands/projects.ts
|
|
7644
|
-
import { resolve as
|
|
7741
|
+
import { resolve as resolve5 } from "node:path";
|
|
7645
7742
|
var WAIT_TIMEOUT_MS = 18e4;
|
|
7646
7743
|
var FIRST_POLL_MS = 500;
|
|
7647
7744
|
var POLL_GROWTH = 1.5;
|
|
@@ -7699,7 +7796,7 @@ async function create(flags, name) {
|
|
|
7699
7796
|
);
|
|
7700
7797
|
}
|
|
7701
7798
|
const ctx = await context(flags.api);
|
|
7702
|
-
const root =
|
|
7799
|
+
const root = resolve5(flags.dir ?? ".");
|
|
7703
7800
|
if (flags.link !== false) {
|
|
7704
7801
|
await assertMayCreateLink(root, { relink: flags.relink });
|
|
7705
7802
|
}
|
|
@@ -7751,7 +7848,7 @@ async function init(flags, name) {
|
|
|
7751
7848
|
);
|
|
7752
7849
|
}
|
|
7753
7850
|
const ctx = await context(flags.api);
|
|
7754
|
-
const root =
|
|
7851
|
+
const root = resolve5(flags.dir ?? ".");
|
|
7755
7852
|
const explicitProject = flags.project;
|
|
7756
7853
|
const linkedProject = !name && !explicitProject ? (await findLink(root))?.projectId : void 0;
|
|
7757
7854
|
if (name && flags.link !== false) {
|
|
@@ -8025,11 +8122,11 @@ async function mcp(flags, which) {
|
|
|
8025
8122
|
// src/commands/db.ts
|
|
8026
8123
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8027
8124
|
import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
8028
|
-
import { dirname as
|
|
8125
|
+
import { dirname as dirname5, isAbsolute, join as join4, resolve as resolve6 } from "node:path";
|
|
8029
8126
|
var DEFAULT_SCHEMA_FILE = "schema.json";
|
|
8030
8127
|
function schemaPath(ctx, flag) {
|
|
8031
|
-
if (flag) return isAbsolute(flag) ? flag :
|
|
8032
|
-
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();
|
|
8033
8130
|
return join4(base, DEFAULT_SCHEMA_FILE);
|
|
8034
8131
|
}
|
|
8035
8132
|
async function readSchema(path) {
|
|
@@ -8539,7 +8636,7 @@ function render4(f) {
|
|
|
8539
8636
|
|
|
8540
8637
|
// src/commands/functions.ts
|
|
8541
8638
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
8542
|
-
import { basename as basename2, dirname as
|
|
8639
|
+
import { basename as basename2, dirname as dirname6, extname as extname2, resolve as resolve7 } from "node:path";
|
|
8543
8640
|
import { build as bundle } from "esbuild";
|
|
8544
8641
|
var NAME = /^[a-z][a-z0-9-]{0,30}$/;
|
|
8545
8642
|
var MAX_FUNCTION_BYTES = 1e6;
|
|
@@ -8620,10 +8717,10 @@ async function deploy(flags, file) {
|
|
|
8620
8717
|
note(paint("dim", " env.DB, env.FILES, env.KV are this project\u2019s own. Nothing else is reachable."));
|
|
8621
8718
|
}
|
|
8622
8719
|
async function bundleFunction(file) {
|
|
8623
|
-
const absolute =
|
|
8720
|
+
const absolute = resolve7(file);
|
|
8624
8721
|
try {
|
|
8625
8722
|
const result = await bundle({
|
|
8626
|
-
absWorkingDir:
|
|
8723
|
+
absWorkingDir: dirname6(absolute),
|
|
8627
8724
|
entryPoints: [absolute],
|
|
8628
8725
|
bundle: true,
|
|
8629
8726
|
write: false,
|
|
@@ -9067,7 +9164,7 @@ function propertyName(name) {
|
|
|
9067
9164
|
|
|
9068
9165
|
// src/commands/deploy.ts
|
|
9069
9166
|
import { readdir as readdir2, readFile as readFile6, stat as stat4 } from "node:fs/promises";
|
|
9070
|
-
import { createHash as
|
|
9167
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
9071
9168
|
import { spawn as spawn2 } from "node:child_process";
|
|
9072
9169
|
import { join as join5, relative as relative2, sep } from "node:path";
|
|
9073
9170
|
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", ".env", ".env.local", "Thumbs.db"]);
|
|
@@ -9238,7 +9335,7 @@ async function buildSite(root, script, display) {
|
|
|
9238
9335
|
const entries = await readdir2(root);
|
|
9239
9336
|
const runner = packageRunner(entries);
|
|
9240
9337
|
if (!isJsonMode()) note(`Running ${runner.label} run ${script}\u2026`);
|
|
9241
|
-
const code = await new Promise((
|
|
9338
|
+
const code = await new Promise((resolve9, reject) => {
|
|
9242
9339
|
const child = spawn2(runner.command, [...runner.args, "run", script], {
|
|
9243
9340
|
cwd: root,
|
|
9244
9341
|
stdio: "inherit",
|
|
@@ -9248,7 +9345,7 @@ async function buildSite(root, script, display) {
|
|
|
9248
9345
|
shell: process.platform === "win32"
|
|
9249
9346
|
});
|
|
9250
9347
|
child.once("error", reject);
|
|
9251
|
-
child.once("close", (status4) =>
|
|
9348
|
+
child.once("close", (status4) => resolve9(status4 ?? 1));
|
|
9252
9349
|
}).catch((err) => {
|
|
9253
9350
|
throw new CliError(
|
|
9254
9351
|
`Could not start ${display ?? `${runner.label} run ${script}`}.`,
|
|
@@ -9308,7 +9405,7 @@ async function collect(dir) {
|
|
|
9308
9405
|
found.push({
|
|
9309
9406
|
path: toUrlPath(relative2(dir, absolute)),
|
|
9310
9407
|
absolute,
|
|
9311
|
-
hash:
|
|
9408
|
+
hash: createHash4("sha256").update(await readFile6(absolute)).digest("hex"),
|
|
9312
9409
|
size: info.size
|
|
9313
9410
|
});
|
|
9314
9411
|
}
|
|
@@ -9778,7 +9875,7 @@ async function remove4(flags, name) {
|
|
|
9778
9875
|
|
|
9779
9876
|
// src/commands/architect.ts
|
|
9780
9877
|
import { readdir as readdir3, readFile as readFile8, stat as stat5 } from "node:fs/promises";
|
|
9781
|
-
import { join as join6, resolve as
|
|
9878
|
+
import { join as join6, resolve as resolve8 } from "node:path";
|
|
9782
9879
|
var FEATURE_PATTERNS = {
|
|
9783
9880
|
auth: /\b(auth(?:entication|enticated|orization|orized)?|log-?ins?|sign-?ins?|sign-?ups?|sessions?|user accounts?|passport|clerk|auth0)\b/i,
|
|
9784
9881
|
storage: /\b(uploads?|attachments?|buckets?|object storage|file (?:uploads?|attachments?|storage)|s3|r2|multer|media)\b/i,
|
|
@@ -9792,7 +9889,7 @@ var FEATURE_PATTERNS = {
|
|
|
9792
9889
|
crawler: /\b(crawls?|crawlers?|scrapes?|playwright|puppeteer|browser automation)\b/i
|
|
9793
9890
|
};
|
|
9794
9891
|
async function architect(flags, ...words) {
|
|
9795
|
-
const root =
|
|
9892
|
+
const root = resolve8(flags.dir ?? ".");
|
|
9796
9893
|
const requirements = words.filter((word) => Boolean(word)).join(" ").trim();
|
|
9797
9894
|
const ctx = await context(flags.api);
|
|
9798
9895
|
if (!flags.project && !ctx.link) {
|