nimblybase 0.1.8 → 0.1.10

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 +27 -0
  2. package/dist/index.js +117 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -100,6 +100,33 @@ credentials**. Commit it: a teammate cloning the repo gets the same target
100
100
  without being told a UUID. Commands run anywhere beneath it use that project
101
101
  unless you name another with `-p`.
102
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. Its JSON
126
+ keeps the shared ready/provisioning/attention-required status and next actions;
127
+ paused projects direct the agent to diagnosis, and default provisioning waits
128
+ remain in effect.
129
+
103
130
  ## Scripting
104
131
 
105
132
  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,
@@ -314,6 +316,8 @@ OPTIONS
314
316
  --level <info|warn|error> Only logs at this level or worse
315
317
  --route </v1/sql> Only logs for one route
316
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
317
321
  --no-link Do not link the directory after creating a project
318
322
  --relink Explicitly replace this repository's project target
319
323
  --with-token Include the live token in \`mcp\` output
@@ -374,7 +378,7 @@ function when(iso) {
374
378
 
375
379
  // src/api.ts
376
380
  var DEFAULT_API_URL = "https://api-uat.nimblybase.com";
377
- var VERSION = "0.1.8";
381
+ var VERSION = "0.1.10";
378
382
  function dashboardUrl(api) {
379
383
  const match = /^https:\/\/api(-[a-z0-9]+)?\.(.+)$/.exec(api);
380
384
  return match ? `https://app${match[1] ?? ""}.${match[2]}` : null;
@@ -1267,8 +1271,8 @@ async function confirm(question) {
1267
1271
  // src/context.ts
1268
1272
  import { realpath } from "node:fs/promises";
1269
1273
  import { dirname as dirname3 } from "node:path";
1270
- async function context(apiOverride) {
1271
- const link2 = await findLink();
1274
+ async function context(apiOverride, directory) {
1275
+ const link2 = await findLink(directory);
1272
1276
  const repositoryStore = link2 ? repositoryCredentials(await realpath(dirname3(link2.path))) : void 0;
1273
1277
  const repositoryToken = await repositoryStore?.load();
1274
1278
  const useRepository = repositoryToken?.where !== "environment" && (Boolean(repositoryToken) || link2?.authorization === "project");
@@ -1362,6 +1366,36 @@ var WHERE_SAID = {
1362
1366
  file: `${credentialsPath()} (permissions 0600)`,
1363
1367
  environment: "the NIMBLYBASE_TOKEN environment variable"
1364
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
+ }
1365
1399
  async function login(flags) {
1366
1400
  const base = apiUrl(flags.api);
1367
1401
  const link2 = await findLink(flags.dir);
@@ -1385,7 +1419,6 @@ async function login(flags) {
1385
1419
  "Use --relink to explicitly authorize and link a different project."
1386
1420
  );
1387
1421
  const root = projectId ? await realpath2(link2 ? dirname4(link2.path) : resolve2(flags.dir ?? ".")) : void 0;
1388
- const store = root ? repositoryCredentials(root) : credentials;
1389
1422
  const given = flags.token ?? await readPipedInput();
1390
1423
  if (projectId && (given || flags.tokenOnly)) throw new CliError(
1391
1424
  "A pasted token does not authorize a repository project.",
@@ -1412,21 +1445,7 @@ It expires in ${expiresInSeconds} seconds. Approve only the CLI request you star
1412
1445
  open: openBrowser,
1413
1446
  ...projectId ? { projectId } : {}
1414
1447
  });
1415
- const client = createClient(base, grant.accessToken);
1416
- const me2 = await client.me();
1417
- if (projectId && (me2.authorization?.via !== "oauth" || me2.authorization.projectId !== projectId)) {
1418
- throw new CliError(
1419
- "The server did not confirm this project restriction.",
1420
- EXIT.unauthenticated,
1421
- "Nothing was saved. Update the server/client and approve project access again."
1422
- );
1423
- }
1424
- if (projectId && root) {
1425
- const { project } = await client.project(projectId);
1426
- if (project.id !== projectId) throw new CliError("The authorized project response is invalid.", EXIT.unauthenticated);
1427
- await writeLink(root, { projectId, slug: project.slug, apiUrl: base, authorization: "project" }, { relink: flags.relink });
1428
- }
1429
- const where2 = await store.saveGrant({ ...grant, issuer: base, ...projectId ? { projectId } : {} });
1448
+ const { me: me2, where: where2 } = await saveBrowserLogin(grant, base, projectId, root, flags.relink);
1430
1449
  if (isJsonMode()) {
1431
1450
  json({
1432
1451
  ok: true,
@@ -7737,6 +7756,45 @@ async function prepareCreation(ctx, flags, name, operation) {
7737
7756
  });
7738
7757
  }
7739
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
+
7740
7798
  // src/commands/projects.ts
7741
7799
  import { resolve as resolve5 } from "node:path";
7742
7800
  var WAIT_TIMEOUT_MS = 18e4;
@@ -7786,6 +7844,7 @@ async function show(flags, ref) {
7786
7844
  ]);
7787
7845
  }
7788
7846
  async function create(flags, name) {
7847
+ validateRepositorySetup(flags);
7789
7848
  if (!name) throw new CliError("A name is required.", EXIT.usage, 'nimbly create "My App"');
7790
7849
  const tier = flags.tier ?? "d1";
7791
7850
  if (tier !== "d1") {
@@ -7795,7 +7854,7 @@ async function create(flags, name) {
7795
7854
  "Use --tier d1."
7796
7855
  );
7797
7856
  }
7798
- const ctx = await context(flags.api);
7857
+ const ctx = await context(flags.api, flags.dir);
7799
7858
  const root = resolve5(flags.dir ?? ".");
7800
7859
  if (flags.link !== false) {
7801
7860
  await assertMayCreateLink(root, { relink: flags.relink });
@@ -7809,23 +7868,27 @@ async function create(flags, name) {
7809
7868
  linkedTo = await writeLink(root, {
7810
7869
  projectId: final.id,
7811
7870
  slug: final.slug,
7812
- ...flags.api ? { apiUrl: flags.api } : {}
7871
+ apiUrl: ctx.base,
7872
+ authorization: "project"
7813
7873
  }, { relink: flags.relink });
7814
7874
  }
7815
7875
  await clearPendingBootstrap(root);
7876
+ const authorization = await finishRepositorySetup(flags, final, linkedTo, ctx.base);
7816
7877
  if (isJsonMode()) {
7817
- json({ project: final, linkedTo });
7878
+ json({ project: final, linkedTo, authorization });
7818
7879
  return;
7819
7880
  }
7820
7881
  out(`${paint("green", "\u2713")} ${final.slug} is ${statusPaint(final.status)}`);
7821
7882
  if (final.dataPlaneUrl) out(` ${final.dataPlaneUrl}`);
7822
7883
  if (linkedTo) note(`Linked this directory (${linkedTo}).`);
7884
+ if (authorization.state === "pending") return;
7823
7885
  if (final.status === "active") {
7824
7886
  note('Next: `nimbly keys create "local dev"` for a key your app can use.');
7825
7887
  }
7826
7888
  for (const line of AFTER_CREATE) note(paint("dim", ` ${line}`));
7827
7889
  }
7828
7890
  async function init(flags, name) {
7891
+ validateRepositorySetup(flags);
7829
7892
  if (name && flags.project) {
7830
7893
  throw new CliError(
7831
7894
  "Choose an existing project or name a new one, not both.",
@@ -7847,13 +7910,35 @@ async function init(flags, name) {
7847
7910
  "Use --tier d1."
7848
7911
  );
7849
7912
  }
7850
- const ctx = await context(flags.api);
7913
+ const ctx = await context(flags.api, flags.dir);
7851
7914
  const root = resolve5(flags.dir ?? ".");
7852
7915
  const explicitProject = flags.project;
7853
7916
  const linkedProject = !name && !explicitProject ? (await findLink(root))?.projectId : void 0;
7854
7917
  if (name && flags.link !== false) {
7855
7918
  await assertMayCreateLink(root, { relink: flags.relink });
7856
7919
  }
7920
+ if (ctx.credentialScope === "repository" && !name) {
7921
+ const initial = await resolveProject(ctx, explicitProject ?? linkedProject);
7922
+ const project = flags.wait === false ? initial : await waitForActive(ctx, initial);
7923
+ const backend = await ctx.client.describeProject(project.id);
7924
+ const status4 = project.status === "active" ? "ready" : project.status === "provisioning" ? "provisioning" : "attention_required";
7925
+ const result2 = { status: status4, project, backend, nextActions: status4 === "ready" ? [
7926
+ { code: "inspect_backend", command: "nimbly describe", description: "Inspect available services before designing the backend." },
7927
+ { code: "plan_schema", command: "nimbly db status", description: "Inspect the schema before applying changes." }
7928
+ ] : status4 === "provisioning" ? [
7929
+ { code: "wait_for_project", command: `nimbly project ${project.slug}`, description: "Wait for the project to become active." }
7930
+ ] : [
7931
+ { code: "diagnose_project", command: `nimbly diagnose ${project.slug}`, description: `The project is ${project.status}; inspect before using it.` }
7932
+ ] };
7933
+ if (isJsonMode()) {
7934
+ json({ ...result2, linkedTo: ctx.link?.path ?? null, authorization: { state: "authorized", projectId: project.id } });
7935
+ } else {
7936
+ out(`${paint("green", "\u2713")} ${project.slug} is ${statusPaint(project.status)}`);
7937
+ note("This repository is already authorized.");
7938
+ for (const action of result2.nextActions) note(`Next: ${action.command} \u2014 ${action.description}`);
7939
+ }
7940
+ return;
7941
+ }
7857
7942
  const pending = name ? await prepareCreation(ctx, flags, name, "init") : void 0;
7858
7943
  const request2 = {
7859
7944
  ...explicitProject || linkedProject ? { project: explicitProject ?? linkedProject } : {},
@@ -7918,16 +8003,19 @@ async function init(flags, name) {
7918
8003
  const linkedTo = flags.link === false ? null : await writeLink(root, {
7919
8004
  projectId: result.project.id,
7920
8005
  slug: result.project.slug,
7921
- ...flags.api ? { apiUrl: flags.api } : {}
8006
+ apiUrl: ctx.base,
8007
+ authorization: "project"
7922
8008
  }, { relink: flags.relink });
7923
8009
  if (pending) await clearPendingBootstrap(root);
8010
+ const authorization = await finishRepositorySetup(flags, result.project, linkedTo, ctx.base);
7924
8011
  if (isJsonMode()) {
7925
- json({ ...result, linkedTo });
8012
+ json({ ...result, linkedTo, authorization });
7926
8013
  return;
7927
8014
  }
7928
8015
  out(`${paint("green", "\u2713")} ${result.project.slug} is ${statusPaint(result.project.status)}`);
7929
8016
  if (result.project.dataPlaneUrl) out(` ${result.project.dataPlaneUrl}`);
7930
8017
  if (linkedTo) note(`Linked this directory (${linkedTo}).`);
8018
+ if (authorization.state === "pending") return;
7931
8019
  for (const action of result.nextActions) note(`Next: ${action.command} \u2014 ${action.description}`);
7932
8020
  }
7933
8021
  async function waitForActive(ctx, project) {
@@ -8122,11 +8210,11 @@ async function mcp(flags, which) {
8122
8210
  // src/commands/db.ts
8123
8211
  import { randomUUID as randomUUID3 } from "node:crypto";
8124
8212
  import { readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
8125
- import { dirname as dirname5, isAbsolute, join as join4, resolve as resolve6 } from "node:path";
8213
+ import { dirname as dirname6, isAbsolute, join as join4, resolve as resolve6 } from "node:path";
8126
8214
  var DEFAULT_SCHEMA_FILE = "schema.json";
8127
8215
  function schemaPath(ctx, flag) {
8128
8216
  if (flag) return isAbsolute(flag) ? flag : resolve6(process.cwd(), flag);
8129
- const base = ctx.link ? dirname5(ctx.link.path) : process.cwd();
8217
+ const base = ctx.link ? dirname6(ctx.link.path) : process.cwd();
8130
8218
  return join4(base, DEFAULT_SCHEMA_FILE);
8131
8219
  }
8132
8220
  async function readSchema(path) {
@@ -8636,7 +8724,7 @@ function render4(f) {
8636
8724
 
8637
8725
  // src/commands/functions.ts
8638
8726
  import { readFile as readFile5 } from "node:fs/promises";
8639
- import { basename as basename2, dirname as dirname6, extname as extname2, resolve as resolve7 } from "node:path";
8727
+ import { basename as basename2, dirname as dirname7, extname as extname2, resolve as resolve7 } from "node:path";
8640
8728
  import { build as bundle } from "esbuild";
8641
8729
  var NAME = /^[a-z][a-z0-9-]{0,30}$/;
8642
8730
  var MAX_FUNCTION_BYTES = 1e6;
@@ -8720,7 +8808,7 @@ async function bundleFunction(file) {
8720
8808
  const absolute = resolve7(file);
8721
8809
  try {
8722
8810
  const result = await bundle({
8723
- absWorkingDir: dirname6(absolute),
8811
+ absWorkingDir: dirname7(absolute),
8724
8812
  entryPoints: [absolute],
8725
8813
  bundle: true,
8726
8814
  write: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nimblybase",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "The NimblyBase command line - projects, keys and backends from a terminal.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {