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.
- package/README.md +27 -0
- package/dist/index.js +117 -29
- 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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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
|
|
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:
|
|
8811
|
+
absWorkingDir: dirname7(absolute),
|
|
8724
8812
|
entryPoints: [absolute],
|
|
8725
8813
|
bundle: true,
|
|
8726
8814
|
write: false,
|