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.
Files changed (3) hide show
  1. package/README.md +45 -0
  2. package/dist/index.js +336 -156
  3. 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.7";
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((resolve8) => setTimeout(resolve8, ms)));
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
- ACCOUNT,
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", ACCOUNT, "-s", SERVICE, "-w"]);
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", ACCOUNT, "-s", SERVICE]);
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
- return JSON.parse(await readFile(credentialsPath(), "utf8"));
976
- } catch {
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
- var credentials = {
996
- async save(token, apiUrl2) {
997
- if (await keychainSet(encodeSecret(token, apiUrl2))) return "keychain";
998
- await fileWrite(token, apiUrl2);
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
- // src/prompt.ts
1044
- import { createInterface } from "node:readline";
1045
- async function readPipedInput() {
1046
- if (process.stdin.isTTY) return null;
1047
- const chunks = [];
1048
- for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
1049
- const value = Buffer.concat(chunks).toString("utf8").trim();
1050
- return value.length > 0 ? value : null;
1051
- }
1052
- function promptSecret(question) {
1053
- return new Promise((resolve8, reject) => {
1054
- const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
1055
- let promptShown = false;
1056
- const output = rl;
1057
- const write = output.output.write.bind(output.output);
1058
- output.output.write = (chunk) => {
1059
- if (!promptShown) {
1060
- promptShown = true;
1061
- return write(chunk);
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
- return true;
1064
- };
1065
- rl.question(question, (answer) => {
1066
- ;
1067
- output.output.write = write;
1068
- process.stderr.write("\n");
1069
- rl.close();
1070
- resolve8(answer.trim());
1071
- });
1072
- rl.on("SIGINT", () => {
1073
- rl.close();
1074
- reject(new Error("cancelled"));
1075
- });
1076
- });
1077
- }
1078
- async function confirmExact(question, expected) {
1079
- if (!process.stdin.isTTY) return false;
1080
- const rl = createInterface({ input: process.stdin, output: process.stderr });
1081
- try {
1082
- const answer = await new Promise((r) => rl.question(`${question}
1083
- Type "${expected}" to confirm: `, r));
1084
- return answer.trim() === expected;
1085
- } finally {
1086
- rl.close();
1087
- }
1088
- }
1089
- async function confirm(question) {
1090
- if (!process.stdin.isTTY) return false;
1091
- const rl = createInterface({ input: process.stdin, output: process.stderr });
1092
- try {
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
- async function context(apiOverride) {
1241
- const link2 = await findLink();
1242
- const storedBeforeRefresh = await credentials.load();
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 credentials.loadGrant();
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 credentials.saveGrant({ ...fresh, issuer: held.grant.issuer });
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 credentials.load();
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 createClient(base, grant.accessToken).me();
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({ ok: true, storedIn: where2, organization: me2.organization, api: base, scopes: grant.scopes });
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(`Stored in ${WHERE_SAID[where2]}. It refreshes itself; run \`nimbly logout\` to end it.`);
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 cleared = await credentials.clear();
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 createHash2 } from "node:crypto";
1416
- import { resolve as resolve3 } from "node:path";
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 resolve2 } from "node:path";
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(resolve2(flags.dir ?? "."), flags, requirements);
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 = resolve3(flags.dir ?? ".");
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 = createHash2("sha256").update(JSON.stringify(SuitabilityFacts.parse(facts))).digest("hex");
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 resolve4 } from "node:path";
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 = resolve4(flags.dir ?? ".");
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
- ...flags.api ? { apiUrl: flags.api } : {}
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 = resolve4(flags.dir ?? ".");
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
- ...flags.api ? { apiUrl: flags.api } : {}
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 dirname3, isAbsolute, join as join4, resolve as resolve5 } from "node:path";
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 : resolve5(process.cwd(), flag);
8032
- const base = ctx.link ? dirname3(ctx.link.path) : process.cwd();
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 dirname4, extname as extname2, resolve as resolve6 } from "node:path";
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 = resolve6(file);
8803
+ const absolute = resolve7(file);
8624
8804
  try {
8625
8805
  const result = await bundle({
8626
- absWorkingDir: dirname4(absolute),
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 createHash3 } from "node:crypto";
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((resolve8, reject) => {
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) => resolve8(status4 ?? 1));
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: createHash3("sha256").update(await readFile6(absolute)).digest("hex"),
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 resolve7 } from "node:path";
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 = resolve7(flags.dir ?? ".");
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nimblybase",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "The NimblyBase command line - projects, keys and backends from a terminal.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {