nimblybase 0.1.7 → 0.1.9
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 +45 -0
- package/dist/index.js +336 -156
- 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
|
|
@@ -79,6 +100,30 @@ credentials**. Commit it: a teammate cloning the repo gets the same target
|
|
|
79
100
|
without being told a UUID. Commands run anywhere beneath it use that project
|
|
80
101
|
unless you name another with `-p`.
|
|
81
102
|
|
|
103
|
+
## Project consent after setup
|
|
104
|
+
|
|
105
|
+
`nimbly init` and `nimbly create` link an active project, then open browser
|
|
106
|
+
consent automatically in an interactive terminal. The verified project grant
|
|
107
|
+
is stored separately for this repository; your workspace login stays available
|
|
108
|
+
for creating another project in another directory. Approval remains your choice.
|
|
109
|
+
|
|
110
|
+
For an AI terminal, use `--authorize` to wait for browser consent explicitly.
|
|
111
|
+
For scripts, `--json`, `--no-authorize`, CI, and a terminal without interactive
|
|
112
|
+
input leave consent pending instead of opening a browser. The JSON response has
|
|
113
|
+
an `authorization` object with `state`, `projectId` and, when pending, a retry
|
|
114
|
+
`command`. Run that command **inside the linked repository**. A pending link
|
|
115
|
+
will not fall back to the wider workspace login.
|
|
116
|
+
|
|
117
|
+
`--no-wait` leaves consent pending while provisioning. `--no-link` creates no
|
|
118
|
+
repository login. An explicit `NIMBLYBASE_TOKEN` is reported as `environment`;
|
|
119
|
+
its permissions are those of that token, with no browser grant created. It
|
|
120
|
+
cannot be combined with `--authorize`.
|
|
121
|
+
|
|
122
|
+
If approval is denied or fails, the project and link are retained. Retry
|
|
123
|
+
`nimbly login` with the API printed by the CLI; do not recreate the project.
|
|
124
|
+
A returning repository grant uses read endpoints for `nimbly init`, so it does
|
|
125
|
+
not need workspace creation permission or another browser approval.
|
|
126
|
+
|
|
82
127
|
## Scripting
|
|
83
128
|
|
|
84
129
|
Every command takes `--json`. In JSON mode stdout carries the document and
|
package/dist/index.js
CHANGED
|
@@ -98,6 +98,7 @@ function parse(argv) {
|
|
|
98
98
|
// SQL.
|
|
99
99
|
param: { type: "string", multiple: true },
|
|
100
100
|
"allow-destructive": { type: "boolean", default: false },
|
|
101
|
+
authorize: { type: "boolean" },
|
|
101
102
|
wait: { type: "boolean", default: true },
|
|
102
103
|
link: { type: "boolean", default: true },
|
|
103
104
|
relink: { type: "boolean", default: false },
|
|
@@ -168,6 +169,7 @@ function parse(argv) {
|
|
|
168
169
|
maxTokens: v["max-tokens"] === void 0 ? void 0 : Number(v["max-tokens"]),
|
|
169
170
|
params: v.param,
|
|
170
171
|
allowDestructive: v["allow-destructive"] === true,
|
|
172
|
+
authorize: v.authorize,
|
|
171
173
|
wait: v.wait,
|
|
172
174
|
link: v.link,
|
|
173
175
|
relink: v.relink === true,
|
|
@@ -185,6 +187,7 @@ USAGE
|
|
|
185
187
|
|
|
186
188
|
GETTING STARTED
|
|
187
189
|
login Sign in through your browser; --device for another machine
|
|
190
|
+
Inside a linked repo, approve only that project
|
|
188
191
|
--token-only is the explicit pasted-PAT fallback
|
|
189
192
|
init [name] Create or explicitly choose a project for this repo
|
|
190
193
|
describe Show what the linked project's backend can do
|
|
@@ -313,6 +316,8 @@ OPTIONS
|
|
|
313
316
|
--level <info|warn|error> Only logs at this level or worse
|
|
314
317
|
--route </v1/sql> Only logs for one route
|
|
315
318
|
--no-wait Do not wait for a new project to finish provisioning
|
|
319
|
+
--authorize Open project consent after linking, including from an agent terminal
|
|
320
|
+
--no-authorize Leave project consent pending; never use workspace login as fallback
|
|
316
321
|
--no-link Do not link the directory after creating a project
|
|
317
322
|
--relink Explicitly replace this repository's project target
|
|
318
323
|
--with-token Include the live token in \`mcp\` output
|
|
@@ -373,7 +378,7 @@ function when(iso) {
|
|
|
373
378
|
|
|
374
379
|
// src/api.ts
|
|
375
380
|
var DEFAULT_API_URL = "https://api-uat.nimblybase.com";
|
|
376
|
-
var VERSION = "0.1.
|
|
381
|
+
var VERSION = "0.1.9";
|
|
377
382
|
function dashboardUrl(api) {
|
|
378
383
|
const match = /^https:\/\/api(-[a-z0-9]+)?\.(.+)$/.exec(api);
|
|
379
384
|
return match ? `https://app${match[1] ?? ""}.${match[2]}` : null;
|
|
@@ -808,7 +813,7 @@ async function deviceLogin(input, fetchImpl = fetch) {
|
|
|
808
813
|
throw new Error("the device verification page is outside the authorization origin");
|
|
809
814
|
}
|
|
810
815
|
const now = input.now ?? (() => performance.now());
|
|
811
|
-
const sleep = input.sleep ?? ((ms) => new Promise((
|
|
816
|
+
const sleep = input.sleep ?? ((ms) => new Promise((resolve9) => setTimeout(resolve9, ms)));
|
|
812
817
|
const deadline = now() + device.expires_in * 1e3;
|
|
813
818
|
let interval = Math.max(5, device.interval ?? 5) * 1e3;
|
|
814
819
|
input.announce({ verificationUri: verification.toString(), userCode: device.user_code, expiresInSeconds: device.expires_in });
|
|
@@ -864,6 +869,7 @@ async function browserLogin(input) {
|
|
|
864
869
|
const port = await listener.port;
|
|
865
870
|
const redirectUri = `http://127.0.0.1:${port}${CALLBACK_PATH}`;
|
|
866
871
|
const authorize = new URL(doc.authorization_endpoint);
|
|
872
|
+
if (input.projectId) authorize.searchParams.set("project_id", input.projectId);
|
|
867
873
|
for (const [k, v] of Object.entries({
|
|
868
874
|
response_type: "code",
|
|
869
875
|
client_id: clientId,
|
|
@@ -905,7 +911,7 @@ import { promisify } from "node:util";
|
|
|
905
911
|
import { homedir } from "node:os";
|
|
906
912
|
import { join, dirname } from "node:path";
|
|
907
913
|
import { mkdir, readFile, writeFile, chmod, rm, stat, rename } from "node:fs/promises";
|
|
908
|
-
import { randomUUID } from "node:crypto";
|
|
914
|
+
import { createHash as createHash2, randomUUID } from "node:crypto";
|
|
909
915
|
var run = promisify(execFile);
|
|
910
916
|
var SERVICE = "nimblybase-cli";
|
|
911
917
|
var ACCOUNT = "access-token";
|
|
@@ -914,17 +920,21 @@ function configDir() {
|
|
|
914
920
|
return xdg ? join(xdg, "nimblybase") : join(homedir(), ".config", "nimblybase");
|
|
915
921
|
}
|
|
916
922
|
var credentialsPath = () => join(configDir(), "credentials.json");
|
|
923
|
+
function repositoryCredentials(root) {
|
|
924
|
+
const id = createHash2("sha256").update(root).digest("hex");
|
|
925
|
+
return createStore("repository-" + id, () => join(configDir(), "repositories", id, "credentials.json"), true);
|
|
926
|
+
}
|
|
917
927
|
function keychainAvailable() {
|
|
918
928
|
return process.platform === "darwin" && process.env.NIMBLYBASE_NO_KEYCHAIN !== "1";
|
|
919
929
|
}
|
|
920
|
-
async function keychainSet(secret) {
|
|
930
|
+
async function keychainSet(secret, account = ACCOUNT) {
|
|
921
931
|
if (!keychainAvailable()) return false;
|
|
922
932
|
try {
|
|
923
933
|
await run("security", [
|
|
924
934
|
"add-generic-password",
|
|
925
935
|
"-U",
|
|
926
936
|
"-a",
|
|
927
|
-
|
|
937
|
+
account,
|
|
928
938
|
"-s",
|
|
929
939
|
SERVICE,
|
|
930
940
|
"-D",
|
|
@@ -937,20 +947,20 @@ async function keychainSet(secret) {
|
|
|
937
947
|
return false;
|
|
938
948
|
}
|
|
939
949
|
}
|
|
940
|
-
async function keychainGet() {
|
|
950
|
+
async function keychainGet(account = ACCOUNT) {
|
|
941
951
|
if (!keychainAvailable()) return null;
|
|
942
952
|
try {
|
|
943
|
-
const { stdout } = await run("security", ["find-generic-password", "-a",
|
|
953
|
+
const { stdout } = await run("security", ["find-generic-password", "-a", account, "-s", SERVICE, "-w"]);
|
|
944
954
|
const value = stdout.trim();
|
|
945
955
|
return value.length > 0 ? value : null;
|
|
946
956
|
} catch {
|
|
947
957
|
return null;
|
|
948
958
|
}
|
|
949
959
|
}
|
|
950
|
-
async function keychainDelete() {
|
|
960
|
+
async function keychainDelete(account = ACCOUNT) {
|
|
951
961
|
if (!keychainAvailable()) return false;
|
|
952
962
|
try {
|
|
953
|
-
await run("security", ["delete-generic-password", "-a",
|
|
963
|
+
await run("security", ["delete-generic-password", "-a", account, "-s", SERVICE]);
|
|
954
964
|
return true;
|
|
955
965
|
} catch {
|
|
956
966
|
return false;
|
|
@@ -970,15 +980,17 @@ function decodeSecret(raw) {
|
|
|
970
980
|
return {};
|
|
971
981
|
}
|
|
972
982
|
}
|
|
973
|
-
async function fileRead() {
|
|
983
|
+
async function fileRead(path = credentialsPath(), strict = false) {
|
|
974
984
|
try {
|
|
975
|
-
|
|
976
|
-
|
|
985
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
986
|
+
if (strict && (!value || typeof value !== "object")) throw new Error("Invalid repository credential");
|
|
987
|
+
return value;
|
|
988
|
+
} catch (err) {
|
|
989
|
+
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
990
|
return null;
|
|
978
991
|
}
|
|
979
992
|
}
|
|
980
|
-
async function fileWrite(value, apiUrl2) {
|
|
981
|
-
const path = credentialsPath();
|
|
993
|
+
async function fileWrite(value, apiUrl2, path = credentialsPath()) {
|
|
982
994
|
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
983
995
|
await chmod(dirname(path), 448);
|
|
984
996
|
const body = typeof value === "string" ? { token: value, ...apiUrl2 ? { apiUrl: apiUrl2 } : {} } : { grant: value };
|
|
@@ -992,110 +1004,67 @@ async function fileWrite(value, apiUrl2) {
|
|
|
992
1004
|
await rm(temporary, { force: true });
|
|
993
1005
|
}
|
|
994
1006
|
}
|
|
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 {
|
|
1007
|
+
function createStore(account = ACCOUNT, path = credentialsPath, projectOnly = false) {
|
|
1008
|
+
function checkProject(grant) {
|
|
1009
|
+
if (projectOnly && (!grant?.projectId || !grant.issuer || !grant.refreshToken || !grant.accessToken)) {
|
|
1010
|
+
throw new Error("Repository authorization is invalid. Run nimbly login here again; your workspace login was kept.");
|
|
1038
1011
|
}
|
|
1039
|
-
return cleared;
|
|
1040
1012
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1013
|
+
return {
|
|
1014
|
+
async save(token, apiUrl2) {
|
|
1015
|
+
if (projectOnly) throw new Error("Repository authorization requires a project-restricted OAuth grant");
|
|
1016
|
+
if (await keychainSet(encodeSecret(token, apiUrl2), account)) return "keychain";
|
|
1017
|
+
await fileWrite(token, apiUrl2, path());
|
|
1018
|
+
return "file";
|
|
1019
|
+
},
|
|
1020
|
+
async saveGrant(grant) {
|
|
1021
|
+
checkProject(grant);
|
|
1022
|
+
if (await keychainSet(encodeSecret(grant), account)) return "keychain";
|
|
1023
|
+
await fileWrite(grant, void 0, path());
|
|
1024
|
+
return "file";
|
|
1025
|
+
},
|
|
1026
|
+
async load() {
|
|
1027
|
+
const fromEnv = process.env.NIMBLYBASE_TOKEN?.trim();
|
|
1028
|
+
if (fromEnv) return { token: fromEnv, where: "environment" };
|
|
1029
|
+
const fromKeychain = await keychainGet(account);
|
|
1030
|
+
if (fromKeychain) {
|
|
1031
|
+
const { token, grant, apiUrl: apiUrl3 } = decodeSecret(fromKeychain);
|
|
1032
|
+
checkProject(grant);
|
|
1033
|
+
const value2 = token ?? grant?.accessToken;
|
|
1034
|
+
if (value2) return { token: value2, where: "keychain", ...apiUrl3 || grant?.issuer ? { apiUrl: apiUrl3 ?? grant.issuer } : {} };
|
|
1062
1035
|
}
|
|
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
|
-
}
|
|
1036
|
+
const fromFile = await fileRead(path(), projectOnly);
|
|
1037
|
+
if (fromFile) checkProject(fromFile.grant);
|
|
1038
|
+
const value = fromFile?.token ?? fromFile?.grant?.accessToken;
|
|
1039
|
+
const apiUrl2 = fromFile?.apiUrl ?? fromFile?.grant?.issuer;
|
|
1040
|
+
return value ? { token: value, where: "file", ...apiUrl2 ? { apiUrl: apiUrl2 } : {} } : null;
|
|
1041
|
+
},
|
|
1042
|
+
async loadGrant() {
|
|
1043
|
+
if (process.env.NIMBLYBASE_TOKEN?.trim()) return null;
|
|
1044
|
+
const fromKeychain = await keychainGet(account);
|
|
1045
|
+
if (fromKeychain) {
|
|
1046
|
+
const { grant } = decodeSecret(fromKeychain);
|
|
1047
|
+
checkProject(grant);
|
|
1048
|
+
return grant ? { grant, where: "keychain" } : null;
|
|
1049
|
+
}
|
|
1050
|
+
const fromFile = await fileRead(path(), projectOnly);
|
|
1051
|
+
if (fromFile) checkProject(fromFile.grant);
|
|
1052
|
+
return fromFile?.grant ? { grant: fromFile.grant, where: "file" } : null;
|
|
1053
|
+
},
|
|
1054
|
+
async clear() {
|
|
1055
|
+
const cleared = [];
|
|
1056
|
+
if (await keychainDelete(account)) cleared.push("keychain");
|
|
1057
|
+
try {
|
|
1058
|
+
await stat(path());
|
|
1059
|
+
await rm(path());
|
|
1060
|
+
cleared.push("file");
|
|
1061
|
+
} catch {
|
|
1062
|
+
}
|
|
1063
|
+
return cleared;
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1098
1066
|
}
|
|
1067
|
+
var credentials = createStore();
|
|
1099
1068
|
|
|
1100
1069
|
// src/link.ts
|
|
1101
1070
|
import { readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2, link as hardLink } from "node:fs/promises";
|
|
@@ -1146,6 +1115,7 @@ async function findLink(from = process.cwd()) {
|
|
|
1146
1115
|
projectId: parsed.projectId,
|
|
1147
1116
|
slug: typeof parsed.slug === "string" ? parsed.slug : "",
|
|
1148
1117
|
apiUrl: typeof parsed.apiUrl === "string" ? parsed.apiUrl : void 0,
|
|
1118
|
+
...parsed.authorization === "project" ? { authorization: "project" } : {},
|
|
1149
1119
|
path
|
|
1150
1120
|
};
|
|
1151
1121
|
}
|
|
@@ -1181,6 +1151,7 @@ async function writeLink(dir, link2, options = {}) {
|
|
|
1181
1151
|
slug: link2.slug
|
|
1182
1152
|
};
|
|
1183
1153
|
if (link2.apiUrl !== void 0) next.apiUrl = link2.apiUrl;
|
|
1154
|
+
if (link2.authorization !== void 0) next.authorization = link2.authorization;
|
|
1184
1155
|
const temporary = join2(dirname2(path), `.${LINK_FILE}.${process.pid}.${randomUUID2()}.tmp`);
|
|
1185
1156
|
try {
|
|
1186
1157
|
await writeFile2(temporary, `${JSON.stringify(next, null, 2)}
|
|
@@ -1236,12 +1207,86 @@ async function clearPendingBootstrap(dir) {
|
|
|
1236
1207
|
await rm2(join2(dir, BOOTSTRAP_FILE), { force: true });
|
|
1237
1208
|
}
|
|
1238
1209
|
|
|
1210
|
+
// src/commands/auth.ts
|
|
1211
|
+
import { realpath as realpath2 } from "node:fs/promises";
|
|
1212
|
+
import { dirname as dirname4, resolve as resolve2 } from "node:path";
|
|
1213
|
+
|
|
1214
|
+
// src/prompt.ts
|
|
1215
|
+
import { createInterface } from "node:readline";
|
|
1216
|
+
async function readPipedInput() {
|
|
1217
|
+
if (process.stdin.isTTY) return null;
|
|
1218
|
+
const chunks = [];
|
|
1219
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
1220
|
+
const value = Buffer.concat(chunks).toString("utf8").trim();
|
|
1221
|
+
return value.length > 0 ? value : null;
|
|
1222
|
+
}
|
|
1223
|
+
function promptSecret(question) {
|
|
1224
|
+
return new Promise((resolve9, reject) => {
|
|
1225
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
1226
|
+
let promptShown = false;
|
|
1227
|
+
const output = rl;
|
|
1228
|
+
const write = output.output.write.bind(output.output);
|
|
1229
|
+
output.output.write = (chunk) => {
|
|
1230
|
+
if (!promptShown) {
|
|
1231
|
+
promptShown = true;
|
|
1232
|
+
return write(chunk);
|
|
1233
|
+
}
|
|
1234
|
+
return true;
|
|
1235
|
+
};
|
|
1236
|
+
rl.question(question, (answer) => {
|
|
1237
|
+
;
|
|
1238
|
+
output.output.write = write;
|
|
1239
|
+
process.stderr.write("\n");
|
|
1240
|
+
rl.close();
|
|
1241
|
+
resolve9(answer.trim());
|
|
1242
|
+
});
|
|
1243
|
+
rl.on("SIGINT", () => {
|
|
1244
|
+
rl.close();
|
|
1245
|
+
reject(new Error("cancelled"));
|
|
1246
|
+
});
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
async function confirmExact(question, expected) {
|
|
1250
|
+
if (!process.stdin.isTTY) return false;
|
|
1251
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1252
|
+
try {
|
|
1253
|
+
const answer = await new Promise((r) => rl.question(`${question}
|
|
1254
|
+
Type "${expected}" to confirm: `, r));
|
|
1255
|
+
return answer.trim() === expected;
|
|
1256
|
+
} finally {
|
|
1257
|
+
rl.close();
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
async function confirm(question) {
|
|
1261
|
+
if (!process.stdin.isTTY) return false;
|
|
1262
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1263
|
+
try {
|
|
1264
|
+
const answer = await new Promise((r) => rl.question(`${question} [y/N] `, r));
|
|
1265
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
1266
|
+
} finally {
|
|
1267
|
+
rl.close();
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1239
1271
|
// src/context.ts
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1272
|
+
import { realpath } from "node:fs/promises";
|
|
1273
|
+
import { dirname as dirname3 } from "node:path";
|
|
1274
|
+
async function context(apiOverride, directory) {
|
|
1275
|
+
const link2 = await findLink(directory);
|
|
1276
|
+
const repositoryStore = link2 ? repositoryCredentials(await realpath(dirname3(link2.path))) : void 0;
|
|
1277
|
+
const repositoryToken = await repositoryStore?.load();
|
|
1278
|
+
const useRepository = repositoryToken?.where !== "environment" && (Boolean(repositoryToken) || link2?.authorization === "project");
|
|
1279
|
+
const store = useRepository ? repositoryStore : credentials;
|
|
1280
|
+
const storedBeforeRefresh = useRepository ? repositoryToken : await credentials.load();
|
|
1243
1281
|
const base = apiUrl(apiOverride ?? process.env.NIMBLYBASE_API_URL ?? link2?.apiUrl ?? (storedBeforeRefresh?.where !== "environment" ? storedBeforeRefresh?.apiUrl : void 0));
|
|
1244
|
-
const held = await
|
|
1282
|
+
const held = await store.loadGrant();
|
|
1283
|
+
if (useRepository && held && held.grant.projectId !== link2?.projectId) {
|
|
1284
|
+
throw new CliError(
|
|
1285
|
+
"This repository login belongs to a different project.",
|
|
1286
|
+
EXIT.unauthenticated,
|
|
1287
|
+
"Run `nimbly login` here to approve the new project. Your other logins were kept."
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1245
1290
|
if (storedBeforeRefresh && storedBeforeRefresh.where !== "environment") {
|
|
1246
1291
|
const bound = apiUrl(storedBeforeRefresh.apiUrl ?? "https://api-uat.nimblybase.com");
|
|
1247
1292
|
if (base !== bound) throw new CliError(
|
|
@@ -1264,11 +1309,11 @@ async function context(apiOverride) {
|
|
|
1264
1309
|
clientId: held.grant.clientId,
|
|
1265
1310
|
refreshToken: held.grant.refreshToken
|
|
1266
1311
|
});
|
|
1267
|
-
await
|
|
1312
|
+
await store.saveGrant({ ...fresh, issuer: held.grant.issuer, ...held.grant.projectId ? { projectId: held.grant.projectId } : {} });
|
|
1268
1313
|
} catch {
|
|
1269
1314
|
}
|
|
1270
1315
|
}
|
|
1271
|
-
const stored = await
|
|
1316
|
+
const stored = await store.load();
|
|
1272
1317
|
if (!stored) {
|
|
1273
1318
|
throw new CliError(
|
|
1274
1319
|
"Not signed in.",
|
|
@@ -1281,7 +1326,8 @@ async function context(apiOverride) {
|
|
|
1281
1326
|
base,
|
|
1282
1327
|
tokenFrom: stored.where,
|
|
1283
1328
|
isCurrentToken: (prefix) => prefix.length > 0 && stored.token.startsWith(prefix),
|
|
1284
|
-
link: link2
|
|
1329
|
+
link: link2,
|
|
1330
|
+
credentialScope: useRepository ? "repository" : "workspace"
|
|
1285
1331
|
};
|
|
1286
1332
|
}
|
|
1287
1333
|
var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
@@ -1320,9 +1366,65 @@ var WHERE_SAID = {
|
|
|
1320
1366
|
file: `${credentialsPath()} (permissions 0600)`,
|
|
1321
1367
|
environment: "the NIMBLYBASE_TOKEN environment variable"
|
|
1322
1368
|
};
|
|
1369
|
+
async function saveBrowserLogin(grant, base, projectId, root, relink) {
|
|
1370
|
+
const client = createClient(base, grant.accessToken);
|
|
1371
|
+
const me = await client.me();
|
|
1372
|
+
if (projectId && (me.authorization?.via !== "oauth" || me.authorization.projectId !== projectId)) {
|
|
1373
|
+
throw new CliError(
|
|
1374
|
+
"The server did not confirm this project restriction.",
|
|
1375
|
+
EXIT.unauthenticated,
|
|
1376
|
+
"Nothing was saved. Update the server/client and approve project access again."
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
if (projectId && root) {
|
|
1380
|
+
const { project } = await client.project(projectId);
|
|
1381
|
+
if (project.id !== projectId) throw new CliError("The authorized project response is invalid.", EXIT.unauthenticated);
|
|
1382
|
+
await writeLink(root, { projectId, slug: project.slug, apiUrl: base, authorization: "project" }, { relink });
|
|
1383
|
+
}
|
|
1384
|
+
const store = root ? repositoryCredentials(root) : credentials;
|
|
1385
|
+
const where = await store.saveGrant({ ...grant, issuer: base, ...projectId ? { projectId } : {} });
|
|
1386
|
+
return { me, where };
|
|
1387
|
+
}
|
|
1388
|
+
async function authorizeRepository(root, projectId, base, relink) {
|
|
1389
|
+
const canonical = await realpath2(root);
|
|
1390
|
+
const grant = await browserLogin({
|
|
1391
|
+
issuer: base,
|
|
1392
|
+
projectId,
|
|
1393
|
+
open: openBrowser,
|
|
1394
|
+
announce: (url) => note(`Approve access to this project in your browser. If it did not open, go to:
|
|
1395
|
+
${url}`)
|
|
1396
|
+
});
|
|
1397
|
+
await saveBrowserLogin(grant, base, projectId, canonical, relink);
|
|
1398
|
+
}
|
|
1323
1399
|
async function login(flags) {
|
|
1324
1400
|
const base = apiUrl(flags.api);
|
|
1401
|
+
const link2 = await findLink(flags.dir);
|
|
1402
|
+
const projectId = flags.project ?? link2?.projectId;
|
|
1403
|
+
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)) {
|
|
1404
|
+
throw new CliError("Project authorization requires a project UUID.", EXIT.usage, "Link this repository first, or pass --project <uuid>.");
|
|
1405
|
+
}
|
|
1406
|
+
if (projectId && flags.device) throw new CliError(
|
|
1407
|
+
"Project sign-in currently requires browser approval.",
|
|
1408
|
+
EXIT.usage,
|
|
1409
|
+
"Run `nimbly login` here without --device. Device sign-in outside linked repositories remains workspace-scoped."
|
|
1410
|
+
);
|
|
1411
|
+
if (projectId && link2?.apiUrl && apiUrl(link2.apiUrl) !== base) throw new CliError(
|
|
1412
|
+
"Choose the linked project API explicitly.",
|
|
1413
|
+
EXIT.usage,
|
|
1414
|
+
"Pass --api with the linked API; a repository file cannot choose your login destination."
|
|
1415
|
+
);
|
|
1416
|
+
if (link2 && projectId !== link2.projectId && !flags.relink) throw new CliError(
|
|
1417
|
+
"This repository is linked to another project.",
|
|
1418
|
+
EXIT.usage,
|
|
1419
|
+
"Use --relink to explicitly authorize and link a different project."
|
|
1420
|
+
);
|
|
1421
|
+
const root = projectId ? await realpath2(link2 ? dirname4(link2.path) : resolve2(flags.dir ?? ".")) : void 0;
|
|
1325
1422
|
const given = flags.token ?? await readPipedInput();
|
|
1423
|
+
if (projectId && (given || flags.tokenOnly)) throw new CliError(
|
|
1424
|
+
"A pasted token does not authorize a repository project.",
|
|
1425
|
+
EXIT.usage,
|
|
1426
|
+
"Use browser sign-in here; use NIMBLYBASE_TOKEN for an explicitly configured automation identity."
|
|
1427
|
+
);
|
|
1326
1428
|
if (flags.device && (given || flags.tokenOnly)) throw new CliError("Choose device sign-in or a pasted token, not both.", EXIT.usage);
|
|
1327
1429
|
if (!given && !flags.tokenOnly) {
|
|
1328
1430
|
const grant = flags.device ? await deviceLogin({
|
|
@@ -1340,17 +1442,26 @@ It expires in ${expiresInSeconds} seconds. Approve only the CLI request you star
|
|
|
1340
1442
|
note(`If it did not open, go to:
|
|
1341
1443
|
${url}`);
|
|
1342
1444
|
},
|
|
1343
|
-
open: openBrowser
|
|
1445
|
+
open: openBrowser,
|
|
1446
|
+
...projectId ? { projectId } : {}
|
|
1344
1447
|
});
|
|
1345
|
-
const me2 = await
|
|
1346
|
-
const where2 = await credentials.saveGrant({ ...grant, issuer: base });
|
|
1448
|
+
const { me: me2, where: where2 } = await saveBrowserLogin(grant, base, projectId, root, flags.relink);
|
|
1347
1449
|
if (isJsonMode()) {
|
|
1348
|
-
json({
|
|
1450
|
+
json({
|
|
1451
|
+
ok: true,
|
|
1452
|
+
storedIn: where2,
|
|
1453
|
+
organization: me2.organization,
|
|
1454
|
+
api: base,
|
|
1455
|
+
scopes: grant.scopes,
|
|
1456
|
+
authorizationScope: projectId ? "repository" : "workspace",
|
|
1457
|
+
...projectId ? { projectId } : {}
|
|
1458
|
+
});
|
|
1349
1459
|
return;
|
|
1350
1460
|
}
|
|
1351
1461
|
out(`${paint("green", "\u2713")} Signed in as ${paint("bold", me2.organization?.name ?? "your account")}${me2.organization ? ` (${me2.organization.role})` : ""}`);
|
|
1352
1462
|
note(`Granted: ${grant.scopes.join(", ") || "no scopes"}.`);
|
|
1353
|
-
note(`
|
|
1463
|
+
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]}.`);
|
|
1464
|
+
note("It refreshes itself; run `nimbly logout` here to clear this login.");
|
|
1354
1465
|
return;
|
|
1355
1466
|
}
|
|
1356
1467
|
const token = given ?? await promptSecret(source(base));
|
|
@@ -1374,14 +1485,18 @@ It expires in ${expiresInSeconds} seconds. Approve only the CLI request you star
|
|
|
1374
1485
|
note("The keychain was not available, so a file was used instead.");
|
|
1375
1486
|
}
|
|
1376
1487
|
}
|
|
1377
|
-
async function logout() {
|
|
1378
|
-
const
|
|
1488
|
+
async function logout(flags) {
|
|
1489
|
+
const link2 = await findLink(flags?.dir);
|
|
1490
|
+
const repo = link2 ? repositoryCredentials(await realpath2(dirname4(link2.path))) : void 0;
|
|
1491
|
+
const repository = link2?.authorization === "project" || repo && await repo.loadGrant() !== null;
|
|
1492
|
+
const cleared = await (repository ? repo : credentials).clear();
|
|
1379
1493
|
if (isJsonMode()) {
|
|
1380
1494
|
json({ ok: true, cleared });
|
|
1381
1495
|
return;
|
|
1382
1496
|
}
|
|
1383
1497
|
if (cleared.length === 0) out("Nothing stored - already signed out.");
|
|
1384
1498
|
else out(`${paint("green", "\u2713")} Signed out (cleared: ${cleared.join(", ")}).`);
|
|
1499
|
+
if (repository) note("Cleared this repository login. Your separate workspace login was kept; run logout outside this repository to clear it.");
|
|
1385
1500
|
if (process.env.NIMBLYBASE_TOKEN) {
|
|
1386
1501
|
note("NIMBLYBASE_TOKEN is still set in this shell and takes precedence - unset it too.");
|
|
1387
1502
|
}
|
|
@@ -1390,7 +1505,7 @@ async function whoami(flags) {
|
|
|
1390
1505
|
const ctx = await context(flags.api);
|
|
1391
1506
|
const me = await ctx.client.me();
|
|
1392
1507
|
if (isJsonMode()) {
|
|
1393
|
-
json({ ...me, api: ctx.base, tokenFrom: ctx.tokenFrom, link: ctx.link });
|
|
1508
|
+
json({ ...me, api: ctx.base, tokenFrom: ctx.tokenFrom, link: ctx.link, credentialScope: ctx.credentialScope });
|
|
1394
1509
|
return;
|
|
1395
1510
|
}
|
|
1396
1511
|
table(["", ""], [
|
|
@@ -1399,6 +1514,7 @@ async function whoami(flags) {
|
|
|
1399
1514
|
["role", me.organization?.role ?? "-"],
|
|
1400
1515
|
["api", ctx.base],
|
|
1401
1516
|
["token from", ctx.tokenFrom],
|
|
1517
|
+
["credential scope", ctx.credentialScope],
|
|
1402
1518
|
["linked project", ctx.link ? `${ctx.link.slug || ctx.link.projectId} \xB7 ${ctx.link.path}` : "none"]
|
|
1403
1519
|
]);
|
|
1404
1520
|
out();
|
|
@@ -1412,8 +1528,8 @@ var AFTER_CREATE = [
|
|
|
1412
1528
|
];
|
|
1413
1529
|
|
|
1414
1530
|
// src/creation-plan.ts
|
|
1415
|
-
import { createHash as
|
|
1416
|
-
import { resolve as
|
|
1531
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1532
|
+
import { resolve as resolve4 } from "node:path";
|
|
1417
1533
|
|
|
1418
1534
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
1419
1535
|
var external_exports = {};
|
|
@@ -7315,7 +7431,7 @@ function staticSite(entries) {
|
|
|
7315
7431
|
|
|
7316
7432
|
// src/commands/assess.ts
|
|
7317
7433
|
import { stat as stat3 } from "node:fs/promises";
|
|
7318
|
-
import { basename, resolve as
|
|
7434
|
+
import { basename, resolve as resolve3 } from "node:path";
|
|
7319
7435
|
|
|
7320
7436
|
// src/repository-scan.ts
|
|
7321
7437
|
import { open, readdir, stat as stat2 } from "node:fs/promises";
|
|
@@ -7554,7 +7670,7 @@ async function assessWithContext(ctx, flags, requirements = "") {
|
|
|
7554
7670
|
return;
|
|
7555
7671
|
}
|
|
7556
7672
|
if (flags.savePlan && !flags.name) throw new CliError("--save-plan requires --name with an explicit project name.", EXIT.usage);
|
|
7557
|
-
const facts = await inspectAssessmentFacts(
|
|
7673
|
+
const facts = await inspectAssessmentFacts(resolve3(flags.dir ?? "."), flags, requirements);
|
|
7558
7674
|
if (flags.savePlan) {
|
|
7559
7675
|
const result2 = ProjectPlanResponse.safeParse(await ctx.client.createProjectPlan({ name: flags.name, ...flags.slug ? { slug: flags.slug } : {}, facts }));
|
|
7560
7676
|
if (!result2.success) throw new CliError("The API returned an invalid project plan.", EXIT.failed);
|
|
@@ -7585,7 +7701,7 @@ async function assess(flags, ...words) {
|
|
|
7585
7701
|
|
|
7586
7702
|
// src/creation-plan.ts
|
|
7587
7703
|
async function prepareCreation(ctx, flags, name, operation) {
|
|
7588
|
-
const root =
|
|
7704
|
+
const root = resolve4(flags.dir ?? ".");
|
|
7589
7705
|
const me = await ctx.client.me();
|
|
7590
7706
|
if (!me.organization) throw new CliError("Select a workspace before creating a project.", EXIT.forbidden);
|
|
7591
7707
|
const previous = await readPendingBootstrap(root);
|
|
@@ -7609,7 +7725,7 @@ async function prepareCreation(ctx, flags, name, operation) {
|
|
|
7609
7725
|
EXIT.usage,
|
|
7610
7726
|
"Run nimbly assess to clarify the requirements. An empty new application needs the explicit --recipe static-d1 choice."
|
|
7611
7727
|
);
|
|
7612
|
-
const fingerprint =
|
|
7728
|
+
const fingerprint = createHash3("sha256").update(JSON.stringify(SuitabilityFacts.parse(facts))).digest("hex");
|
|
7613
7729
|
if (previous) {
|
|
7614
7730
|
if (flags.plan && flags.plan !== previous.planId) throw new CliError("Retry with the original pending plan.", EXIT.usage);
|
|
7615
7731
|
if (fingerprint !== previous.factsFingerprint) throw new CliError(
|
|
@@ -7640,8 +7756,47 @@ async function prepareCreation(ctx, flags, name, operation) {
|
|
|
7640
7756
|
});
|
|
7641
7757
|
}
|
|
7642
7758
|
|
|
7759
|
+
// src/repository-setup.ts
|
|
7760
|
+
import { realpath as realpath3 } from "node:fs/promises";
|
|
7761
|
+
import { dirname as dirname5 } from "node:path";
|
|
7762
|
+
function validateRepositorySetup(flags) {
|
|
7763
|
+
if (flags.authorize && flags.link === false) {
|
|
7764
|
+
throw new CliError("--authorize requires a linked repository.", EXIT.usage);
|
|
7765
|
+
}
|
|
7766
|
+
if (flags.authorize && process.env.NIMBLYBASE_TOKEN) {
|
|
7767
|
+
throw new CliError(
|
|
7768
|
+
"Choose browser consent or the supplied automation token.",
|
|
7769
|
+
EXIT.usage,
|
|
7770
|
+
"Unset NIMBLYBASE_TOKEN before requesting browser consent, or omit --authorize."
|
|
7771
|
+
);
|
|
7772
|
+
}
|
|
7773
|
+
}
|
|
7774
|
+
async function finishRepositorySetup(flags, project, linkedTo, base) {
|
|
7775
|
+
const projectId = project.id;
|
|
7776
|
+
if (!linkedTo) return { state: "not_linked", projectId };
|
|
7777
|
+
if (process.env.NIMBLYBASE_TOKEN) return { state: "environment", projectId };
|
|
7778
|
+
const command = `nimbly login --api ${base}`;
|
|
7779
|
+
const interactive = process.stdin.isTTY === true && process.stderr.isTTY === true && !flags.json && !process.env.CI;
|
|
7780
|
+
if (project.status !== "active" || !(flags.authorize ?? interactive)) {
|
|
7781
|
+
note(`Project authorization is pending. After the project is active, run ${command} in this repository. Your workspace login was kept.`);
|
|
7782
|
+
return { state: "pending", projectId, command };
|
|
7783
|
+
}
|
|
7784
|
+
note(`${project.slug} (${project.id}) is active and linked. Completing repository consent\u2026`);
|
|
7785
|
+
try {
|
|
7786
|
+
await authorizeRepository(await realpath3(dirname5(linkedTo)), projectId, base, flags.relink);
|
|
7787
|
+
} catch {
|
|
7788
|
+
throw new CliError(
|
|
7789
|
+
`${project.slug} (${project.id}) exists and is linked, but project authorization was not completed.`,
|
|
7790
|
+
EXIT.unauthenticated,
|
|
7791
|
+
`Run ${command} here to retry consent. Do not create the project again. Your workspace login was kept.`
|
|
7792
|
+
);
|
|
7793
|
+
}
|
|
7794
|
+
note("Project authorization saved separately for this repository. Your workspace login was kept.");
|
|
7795
|
+
return { state: "authorized", projectId };
|
|
7796
|
+
}
|
|
7797
|
+
|
|
7643
7798
|
// src/commands/projects.ts
|
|
7644
|
-
import { resolve as
|
|
7799
|
+
import { resolve as resolve5 } from "node:path";
|
|
7645
7800
|
var WAIT_TIMEOUT_MS = 18e4;
|
|
7646
7801
|
var FIRST_POLL_MS = 500;
|
|
7647
7802
|
var POLL_GROWTH = 1.5;
|
|
@@ -7689,6 +7844,7 @@ async function show(flags, ref) {
|
|
|
7689
7844
|
]);
|
|
7690
7845
|
}
|
|
7691
7846
|
async function create(flags, name) {
|
|
7847
|
+
validateRepositorySetup(flags);
|
|
7692
7848
|
if (!name) throw new CliError("A name is required.", EXIT.usage, 'nimbly create "My App"');
|
|
7693
7849
|
const tier = flags.tier ?? "d1";
|
|
7694
7850
|
if (tier !== "d1") {
|
|
@@ -7698,8 +7854,8 @@ async function create(flags, name) {
|
|
|
7698
7854
|
"Use --tier d1."
|
|
7699
7855
|
);
|
|
7700
7856
|
}
|
|
7701
|
-
const ctx = await context(flags.api);
|
|
7702
|
-
const root =
|
|
7857
|
+
const ctx = await context(flags.api, flags.dir);
|
|
7858
|
+
const root = resolve5(flags.dir ?? ".");
|
|
7703
7859
|
if (flags.link !== false) {
|
|
7704
7860
|
await assertMayCreateLink(root, { relink: flags.relink });
|
|
7705
7861
|
}
|
|
@@ -7712,23 +7868,27 @@ async function create(flags, name) {
|
|
|
7712
7868
|
linkedTo = await writeLink(root, {
|
|
7713
7869
|
projectId: final.id,
|
|
7714
7870
|
slug: final.slug,
|
|
7715
|
-
|
|
7871
|
+
apiUrl: ctx.base,
|
|
7872
|
+
authorization: "project"
|
|
7716
7873
|
}, { relink: flags.relink });
|
|
7717
7874
|
}
|
|
7718
7875
|
await clearPendingBootstrap(root);
|
|
7876
|
+
const authorization = await finishRepositorySetup(flags, final, linkedTo, ctx.base);
|
|
7719
7877
|
if (isJsonMode()) {
|
|
7720
|
-
json({ project: final, linkedTo });
|
|
7878
|
+
json({ project: final, linkedTo, authorization });
|
|
7721
7879
|
return;
|
|
7722
7880
|
}
|
|
7723
7881
|
out(`${paint("green", "\u2713")} ${final.slug} is ${statusPaint(final.status)}`);
|
|
7724
7882
|
if (final.dataPlaneUrl) out(` ${final.dataPlaneUrl}`);
|
|
7725
7883
|
if (linkedTo) note(`Linked this directory (${linkedTo}).`);
|
|
7884
|
+
if (authorization.state === "pending") return;
|
|
7726
7885
|
if (final.status === "active") {
|
|
7727
7886
|
note('Next: `nimbly keys create "local dev"` for a key your app can use.');
|
|
7728
7887
|
}
|
|
7729
7888
|
for (const line of AFTER_CREATE) note(paint("dim", ` ${line}`));
|
|
7730
7889
|
}
|
|
7731
7890
|
async function init(flags, name) {
|
|
7891
|
+
validateRepositorySetup(flags);
|
|
7732
7892
|
if (name && flags.project) {
|
|
7733
7893
|
throw new CliError(
|
|
7734
7894
|
"Choose an existing project or name a new one, not both.",
|
|
@@ -7750,13 +7910,30 @@ async function init(flags, name) {
|
|
|
7750
7910
|
"Use --tier d1."
|
|
7751
7911
|
);
|
|
7752
7912
|
}
|
|
7753
|
-
const ctx = await context(flags.api);
|
|
7754
|
-
const root =
|
|
7913
|
+
const ctx = await context(flags.api, flags.dir);
|
|
7914
|
+
const root = resolve5(flags.dir ?? ".");
|
|
7755
7915
|
const explicitProject = flags.project;
|
|
7756
7916
|
const linkedProject = !name && !explicitProject ? (await findLink(root))?.projectId : void 0;
|
|
7757
7917
|
if (name && flags.link !== false) {
|
|
7758
7918
|
await assertMayCreateLink(root, { relink: flags.relink });
|
|
7759
7919
|
}
|
|
7920
|
+
if (ctx.credentialScope === "repository" && !name) {
|
|
7921
|
+
const project = await resolveProject(ctx, explicitProject ?? linkedProject);
|
|
7922
|
+
const backend = await ctx.client.describeProject(project.id);
|
|
7923
|
+
if (isJsonMode()) {
|
|
7924
|
+
json({
|
|
7925
|
+
status: project.status === "active" ? "ready" : project.status,
|
|
7926
|
+
project,
|
|
7927
|
+
backend,
|
|
7928
|
+
linkedTo: ctx.link?.path ?? null,
|
|
7929
|
+
authorization: { state: "authorized", projectId: project.id }
|
|
7930
|
+
});
|
|
7931
|
+
} else {
|
|
7932
|
+
out(`${paint("green", "\u2713")} ${project.slug} is ${statusPaint(project.status)}`);
|
|
7933
|
+
note("This repository is already authorized. Next: nimbly describe.");
|
|
7934
|
+
}
|
|
7935
|
+
return;
|
|
7936
|
+
}
|
|
7760
7937
|
const pending = name ? await prepareCreation(ctx, flags, name, "init") : void 0;
|
|
7761
7938
|
const request2 = {
|
|
7762
7939
|
...explicitProject || linkedProject ? { project: explicitProject ?? linkedProject } : {},
|
|
@@ -7821,16 +7998,19 @@ async function init(flags, name) {
|
|
|
7821
7998
|
const linkedTo = flags.link === false ? null : await writeLink(root, {
|
|
7822
7999
|
projectId: result.project.id,
|
|
7823
8000
|
slug: result.project.slug,
|
|
7824
|
-
|
|
8001
|
+
apiUrl: ctx.base,
|
|
8002
|
+
authorization: "project"
|
|
7825
8003
|
}, { relink: flags.relink });
|
|
7826
8004
|
if (pending) await clearPendingBootstrap(root);
|
|
8005
|
+
const authorization = await finishRepositorySetup(flags, result.project, linkedTo, ctx.base);
|
|
7827
8006
|
if (isJsonMode()) {
|
|
7828
|
-
json({ ...result, linkedTo });
|
|
8007
|
+
json({ ...result, linkedTo, authorization });
|
|
7829
8008
|
return;
|
|
7830
8009
|
}
|
|
7831
8010
|
out(`${paint("green", "\u2713")} ${result.project.slug} is ${statusPaint(result.project.status)}`);
|
|
7832
8011
|
if (result.project.dataPlaneUrl) out(` ${result.project.dataPlaneUrl}`);
|
|
7833
8012
|
if (linkedTo) note(`Linked this directory (${linkedTo}).`);
|
|
8013
|
+
if (authorization.state === "pending") return;
|
|
7834
8014
|
for (const action of result.nextActions) note(`Next: ${action.command} \u2014 ${action.description}`);
|
|
7835
8015
|
}
|
|
7836
8016
|
async function waitForActive(ctx, project) {
|
|
@@ -8025,11 +8205,11 @@ async function mcp(flags, which) {
|
|
|
8025
8205
|
// src/commands/db.ts
|
|
8026
8206
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
8027
8207
|
import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
8028
|
-
import { dirname as
|
|
8208
|
+
import { dirname as dirname6, isAbsolute, join as join4, resolve as resolve6 } from "node:path";
|
|
8029
8209
|
var DEFAULT_SCHEMA_FILE = "schema.json";
|
|
8030
8210
|
function schemaPath(ctx, flag) {
|
|
8031
|
-
if (flag) return isAbsolute(flag) ? flag :
|
|
8032
|
-
const base = ctx.link ?
|
|
8211
|
+
if (flag) return isAbsolute(flag) ? flag : resolve6(process.cwd(), flag);
|
|
8212
|
+
const base = ctx.link ? dirname6(ctx.link.path) : process.cwd();
|
|
8033
8213
|
return join4(base, DEFAULT_SCHEMA_FILE);
|
|
8034
8214
|
}
|
|
8035
8215
|
async function readSchema(path) {
|
|
@@ -8539,7 +8719,7 @@ function render4(f) {
|
|
|
8539
8719
|
|
|
8540
8720
|
// src/commands/functions.ts
|
|
8541
8721
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
8542
|
-
import { basename as basename2, dirname as
|
|
8722
|
+
import { basename as basename2, dirname as dirname7, extname as extname2, resolve as resolve7 } from "node:path";
|
|
8543
8723
|
import { build as bundle } from "esbuild";
|
|
8544
8724
|
var NAME = /^[a-z][a-z0-9-]{0,30}$/;
|
|
8545
8725
|
var MAX_FUNCTION_BYTES = 1e6;
|
|
@@ -8620,10 +8800,10 @@ async function deploy(flags, file) {
|
|
|
8620
8800
|
note(paint("dim", " env.DB, env.FILES, env.KV are this project\u2019s own. Nothing else is reachable."));
|
|
8621
8801
|
}
|
|
8622
8802
|
async function bundleFunction(file) {
|
|
8623
|
-
const absolute =
|
|
8803
|
+
const absolute = resolve7(file);
|
|
8624
8804
|
try {
|
|
8625
8805
|
const result = await bundle({
|
|
8626
|
-
absWorkingDir:
|
|
8806
|
+
absWorkingDir: dirname7(absolute),
|
|
8627
8807
|
entryPoints: [absolute],
|
|
8628
8808
|
bundle: true,
|
|
8629
8809
|
write: false,
|
|
@@ -9067,7 +9247,7 @@ function propertyName(name) {
|
|
|
9067
9247
|
|
|
9068
9248
|
// src/commands/deploy.ts
|
|
9069
9249
|
import { readdir as readdir2, readFile as readFile6, stat as stat4 } from "node:fs/promises";
|
|
9070
|
-
import { createHash as
|
|
9250
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
9071
9251
|
import { spawn as spawn2 } from "node:child_process";
|
|
9072
9252
|
import { join as join5, relative as relative2, sep } from "node:path";
|
|
9073
9253
|
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", ".env", ".env.local", "Thumbs.db"]);
|
|
@@ -9238,7 +9418,7 @@ async function buildSite(root, script, display) {
|
|
|
9238
9418
|
const entries = await readdir2(root);
|
|
9239
9419
|
const runner = packageRunner(entries);
|
|
9240
9420
|
if (!isJsonMode()) note(`Running ${runner.label} run ${script}\u2026`);
|
|
9241
|
-
const code = await new Promise((
|
|
9421
|
+
const code = await new Promise((resolve9, reject) => {
|
|
9242
9422
|
const child = spawn2(runner.command, [...runner.args, "run", script], {
|
|
9243
9423
|
cwd: root,
|
|
9244
9424
|
stdio: "inherit",
|
|
@@ -9248,7 +9428,7 @@ async function buildSite(root, script, display) {
|
|
|
9248
9428
|
shell: process.platform === "win32"
|
|
9249
9429
|
});
|
|
9250
9430
|
child.once("error", reject);
|
|
9251
|
-
child.once("close", (status4) =>
|
|
9431
|
+
child.once("close", (status4) => resolve9(status4 ?? 1));
|
|
9252
9432
|
}).catch((err) => {
|
|
9253
9433
|
throw new CliError(
|
|
9254
9434
|
`Could not start ${display ?? `${runner.label} run ${script}`}.`,
|
|
@@ -9308,7 +9488,7 @@ async function collect(dir) {
|
|
|
9308
9488
|
found.push({
|
|
9309
9489
|
path: toUrlPath(relative2(dir, absolute)),
|
|
9310
9490
|
absolute,
|
|
9311
|
-
hash:
|
|
9491
|
+
hash: createHash4("sha256").update(await readFile6(absolute)).digest("hex"),
|
|
9312
9492
|
size: info.size
|
|
9313
9493
|
});
|
|
9314
9494
|
}
|
|
@@ -9778,7 +9958,7 @@ async function remove4(flags, name) {
|
|
|
9778
9958
|
|
|
9779
9959
|
// src/commands/architect.ts
|
|
9780
9960
|
import { readdir as readdir3, readFile as readFile8, stat as stat5 } from "node:fs/promises";
|
|
9781
|
-
import { join as join6, resolve as
|
|
9961
|
+
import { join as join6, resolve as resolve8 } from "node:path";
|
|
9782
9962
|
var FEATURE_PATTERNS = {
|
|
9783
9963
|
auth: /\b(auth(?:entication|enticated|orization|orized)?|log-?ins?|sign-?ins?|sign-?ups?|sessions?|user accounts?|passport|clerk|auth0)\b/i,
|
|
9784
9964
|
storage: /\b(uploads?|attachments?|buckets?|object storage|file (?:uploads?|attachments?|storage)|s3|r2|multer|media)\b/i,
|
|
@@ -9792,7 +9972,7 @@ var FEATURE_PATTERNS = {
|
|
|
9792
9972
|
crawler: /\b(crawls?|crawlers?|scrapes?|playwright|puppeteer|browser automation)\b/i
|
|
9793
9973
|
};
|
|
9794
9974
|
async function architect(flags, ...words) {
|
|
9795
|
-
const root =
|
|
9975
|
+
const root = resolve8(flags.dir ?? ".");
|
|
9796
9976
|
const requirements = words.filter((word) => Boolean(word)).join(" ").trim();
|
|
9797
9977
|
const ctx = await context(flags.api);
|
|
9798
9978
|
if (!flags.project && !ctx.link) {
|