naro-cli 0.27.0 → 0.29.0
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 +61 -7
- package/dist/args.d.ts +2 -0
- package/dist/artifact-types.d.ts +1 -1
- package/dist/{chunk-UGJFIEH7.js → chunk-BAP7PHBF.js} +847 -910
- package/dist/deploy.d.ts +11 -2
- package/dist/deploy.js +7 -1
- package/dist/index.js +244 -33
- package/dist/user-adapter.d.ts +32 -0
- package/dist/working-tree.d.ts +1 -1
- package/package.json +5 -6
package/dist/deploy.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type CacheTransport, type LocalBuildDependencies } from "@next-deploy/b
|
|
|
2
2
|
import { type CliGitConnection } from "@next-deploy/sdk";
|
|
3
3
|
import type { BuildObserver, DeploymentArtifact, DeploymentStatus, DeploymentStrategyRecord } from "./artifact-types";
|
|
4
4
|
import { type GitInfo } from "./git-info";
|
|
5
|
+
import { type UserAdapterChoice } from "./user-adapter";
|
|
5
6
|
import { type WorkingTreeCopy } from "./working-tree";
|
|
6
7
|
/** The slice of LocalBuildExecutor the deploy flow needs — injectable in tests. */
|
|
7
8
|
export interface DeployBuildExecutor {
|
|
@@ -14,6 +15,7 @@ export interface DeployBuildExecutor {
|
|
|
14
15
|
env: Record<string, string>;
|
|
15
16
|
strategy?: DeploymentStrategyRecord | null;
|
|
16
17
|
vinextEnabled?: boolean;
|
|
18
|
+
userAdapter?: "replace" | "keep" | null;
|
|
17
19
|
}, observer: BuildObserver, cache?: CacheTransport): Promise<{
|
|
18
20
|
artifact: DeploymentArtifact;
|
|
19
21
|
}>;
|
|
@@ -75,7 +77,7 @@ export interface DeployOptions {
|
|
|
75
77
|
* enough to time some out when the whole suite ran at once.
|
|
76
78
|
*/
|
|
77
79
|
pollIntervalMs?: number;
|
|
78
|
-
/** Test seam: replaces the local
|
|
80
|
+
/** Test seam: replaces the local build. */
|
|
79
81
|
executor?: DeployBuildExecutor;
|
|
80
82
|
/** Test seam: replaces the `git` calls that read the repository. */
|
|
81
83
|
gitInfo?: (dir: string) => Promise<GitInfo>;
|
|
@@ -84,6 +86,12 @@ export interface DeployOptions {
|
|
|
84
86
|
* 대화형 CLI만 전달한다 — MCP·CI는 생략하고 안내만 받는다.
|
|
85
87
|
*/
|
|
86
88
|
onMissingEnv?: (keys: string[]) => Promise<Record<string, string>>;
|
|
89
|
+
/**
|
|
90
|
+
* The answer to the adapter question (`--user-adapter`, the prompt, MCP's
|
|
91
|
+
* userAdapter), saved to the project once the deployment exists when it
|
|
92
|
+
* differs from the stored choice (spec 2026-11-07 S3).
|
|
93
|
+
*/
|
|
94
|
+
userAdapter?: UserAdapterChoice | undefined;
|
|
87
95
|
}
|
|
88
96
|
export interface DeployResult {
|
|
89
97
|
deploymentId: string;
|
|
@@ -105,7 +113,7 @@ export interface DeployResult {
|
|
|
105
113
|
}
|
|
106
114
|
/**
|
|
107
115
|
* The full CLI deploy: authenticate, register the deployment with the
|
|
108
|
-
* control plane, build locally (
|
|
116
|
+
* control plane, build locally (vinext for Next.js, the static pipeline
|
|
109
117
|
* for Vite/CRA/static sites), upload the artifact and poll until the
|
|
110
118
|
* platform finishes publishing the worker.
|
|
111
119
|
*
|
|
@@ -115,3 +123,4 @@ export declare function deployDirectory(sourceDir: string, options: DeployOption
|
|
|
115
123
|
export { lintDatabase, type DatabaseLint } from "./commands/db-lint";
|
|
116
124
|
export { readCapped } from "./functions/read-capped";
|
|
117
125
|
export { assertFunctionsNode, deployFunctions, type DeployedFunction, type DeployFunctionsOptions, type DeployFunctionsResult, } from "./functions/deploy";
|
|
126
|
+
export { detectUserAdapter, parseUserAdapter, userAdapterUnreadable, type DetectedUserAdapter, type UserAdapterChoice, } from "./user-adapter";
|
package/dist/deploy.js
CHANGED
|
@@ -3,26 +3,32 @@ import {
|
|
|
3
3
|
assertFunctionsNode,
|
|
4
4
|
deployDirectory,
|
|
5
5
|
deployFunctions,
|
|
6
|
+
detectUserAdapter,
|
|
6
7
|
eventsObserver,
|
|
7
8
|
lintDatabase,
|
|
8
9
|
localDependencies,
|
|
9
10
|
parseDotenv,
|
|
11
|
+
parseUserAdapter,
|
|
10
12
|
readCapped,
|
|
11
13
|
readProjectEnv,
|
|
12
14
|
slugify,
|
|
15
|
+
userAdapterUnreadable,
|
|
13
16
|
workingTreeNotices
|
|
14
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-BAP7PHBF.js";
|
|
15
18
|
import "./chunk-K3NQKI34.js";
|
|
16
19
|
export {
|
|
17
20
|
assertFunctionsNode,
|
|
18
21
|
deployDirectory,
|
|
19
22
|
deployFunctions,
|
|
23
|
+
detectUserAdapter,
|
|
20
24
|
eventsObserver,
|
|
21
25
|
lintDatabase,
|
|
22
26
|
localDependencies,
|
|
23
27
|
parseDotenv,
|
|
28
|
+
parseUserAdapter,
|
|
24
29
|
readCapped,
|
|
25
30
|
readProjectEnv,
|
|
26
31
|
slugify,
|
|
32
|
+
userAdapterUnreadable,
|
|
27
33
|
workingTreeNotices
|
|
28
34
|
};
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
STORAGE_OBJECTS_TABLE,
|
|
25
25
|
STORAGE_SUBQUERIES_SQL,
|
|
26
26
|
UNSHOWN_WARNING_LINE,
|
|
27
|
+
USER_ADAPTER_OPTIONS,
|
|
27
28
|
appliedMigrations,
|
|
28
29
|
applyMigrationFiles,
|
|
29
30
|
assertFunctionsNode,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
deployDirectory,
|
|
49
50
|
deployFunctions,
|
|
50
51
|
detectGitInfo,
|
|
52
|
+
detectUserAdapter,
|
|
51
53
|
enableBackendCommand,
|
|
52
54
|
ensureGitignoreHasNaro,
|
|
53
55
|
envFlag,
|
|
@@ -81,6 +83,7 @@ import {
|
|
|
81
83
|
parsePolicyNode,
|
|
82
84
|
parsePolicyStatement,
|
|
83
85
|
parseRedirectEntry,
|
|
86
|
+
parseUserAdapter,
|
|
84
87
|
pendingMigrationNames,
|
|
85
88
|
policiesForTypes,
|
|
86
89
|
policiesLostByReset,
|
|
@@ -124,12 +127,16 @@ import {
|
|
|
124
127
|
toNaroError,
|
|
125
128
|
unreadablePolicyComment,
|
|
126
129
|
usageMonthOf,
|
|
130
|
+
userAdapterQuestion,
|
|
131
|
+
userAdapterStored,
|
|
132
|
+
userAdapterUnasked,
|
|
133
|
+
userAdapterUnreadable,
|
|
127
134
|
userForAccessToken,
|
|
128
135
|
version,
|
|
129
136
|
warningLines,
|
|
130
137
|
withFunctionRetries,
|
|
131
138
|
writeProjectLink
|
|
132
|
-
} from "./chunk-
|
|
139
|
+
} from "./chunk-BAP7PHBF.js";
|
|
133
140
|
import "./chunk-K3NQKI34.js";
|
|
134
141
|
|
|
135
142
|
// src/args.ts
|
|
@@ -257,6 +264,7 @@ var STRING_FLAGS = {
|
|
|
257
264
|
"--build-command": "buildCommand",
|
|
258
265
|
"--output-dir": "outputDir",
|
|
259
266
|
"--vinext": "vinext",
|
|
267
|
+
"--user-adapter": "userAdapter",
|
|
260
268
|
"--expires": "expires",
|
|
261
269
|
"--verify-jwt": "verifyJwt",
|
|
262
270
|
"--method": "method",
|
|
@@ -1696,12 +1704,12 @@ async function allUsers(client, projectId) {
|
|
|
1696
1704
|
const users = [];
|
|
1697
1705
|
let total = 0;
|
|
1698
1706
|
for (let page = 1; ; page++) {
|
|
1699
|
-
const
|
|
1707
|
+
const listed2 = await usersCall(
|
|
1700
1708
|
() => client.backend.listUsers(projectId, { page, perPage: PAGE_SIZE })
|
|
1701
1709
|
);
|
|
1702
|
-
total =
|
|
1703
|
-
users.push(...
|
|
1704
|
-
if (
|
|
1710
|
+
total = listed2.total;
|
|
1711
|
+
users.push(...listed2.users);
|
|
1712
|
+
if (listed2.users.length < PAGE_SIZE || users.length >= total)
|
|
1705
1713
|
return { users, total };
|
|
1706
1714
|
}
|
|
1707
1715
|
}
|
|
@@ -2357,8 +2365,8 @@ function chunk(items3, size) {
|
|
|
2357
2365
|
}
|
|
2358
2366
|
async function readSchema(client, projectId) {
|
|
2359
2367
|
const schema = await client.db.query(projectId, { sql: SCHEMA_SQL });
|
|
2360
|
-
const [
|
|
2361
|
-
if (
|
|
2368
|
+
const [listed2, virtual, internal] = schema.statements;
|
|
2369
|
+
if (listed2 === void 0 || virtual === void 0 || internal === void 0)
|
|
2362
2370
|
throw new NaroError(
|
|
2363
2371
|
"DATABASE_QUERY_FAILED",
|
|
2364
2372
|
`The schema could not be read: the database answered with ${String(schema.statements.length)} result sets instead of 3.`
|
|
@@ -2369,9 +2377,9 @@ async function readSchema(client, projectId) {
|
|
|
2369
2377
|
const virtualNames = new Set(
|
|
2370
2378
|
virtual.rows.map((row) => cellText(virtual, row, "name").toLowerCase())
|
|
2371
2379
|
);
|
|
2372
|
-
const objects2 =
|
|
2373
|
-
const name = cellText(
|
|
2374
|
-
const type = cellText(
|
|
2380
|
+
const objects2 = listed2.rows.map((row) => {
|
|
2381
|
+
const name = cellText(listed2, row, "name");
|
|
2382
|
+
const type = cellText(listed2, row, "type");
|
|
2375
2383
|
if (type === "view" || type === "trigger") return { type, name };
|
|
2376
2384
|
return {
|
|
2377
2385
|
type: virtualNames.has(name.toLowerCase()) ? "virtual" : "table",
|
|
@@ -3239,6 +3247,72 @@ function cellOf(value) {
|
|
|
3239
3247
|
if (typeof value === "string") return value;
|
|
3240
3248
|
return isInvalid(value) ? "invalid" : "condition";
|
|
3241
3249
|
}
|
|
3250
|
+
var SET_POLICY_NAMES = new Set(
|
|
3251
|
+
POLICY_ACTIONS.map((action) => `naro_${action}`)
|
|
3252
|
+
);
|
|
3253
|
+
function customPolicies(named) {
|
|
3254
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
3255
|
+
for (const policy of named) {
|
|
3256
|
+
if (SET_POLICY_NAMES.has(policy.name)) continue;
|
|
3257
|
+
const invalid = policy.invalid !== null;
|
|
3258
|
+
if (!invalid && policy.as !== "permissive") continue;
|
|
3259
|
+
const key = asciiLower(policy.table);
|
|
3260
|
+
const list = byTable.get(key) ?? [];
|
|
3261
|
+
list.push({ name: policy.name, command: policy.command, invalid });
|
|
3262
|
+
byTable.set(key, list);
|
|
3263
|
+
}
|
|
3264
|
+
return byTable;
|
|
3265
|
+
}
|
|
3266
|
+
function covers(policy, action) {
|
|
3267
|
+
return policy.command === "all" || policy.command === action;
|
|
3268
|
+
}
|
|
3269
|
+
var CUSTOM_NOTE = "custom: a policy written as CREATE POLICY allows it. naro policy list shows it.";
|
|
3270
|
+
var INVALID_NOTE = "invalid: a policy that cannot be read covers it, so the Data API refuses it (500 POLICY_INVALID). naro policy list shows why.";
|
|
3271
|
+
var NAMES_ONLY_NOTE = "`none` may not mean denied on those tables: naro policy list shows their policies.";
|
|
3272
|
+
function listed(entries) {
|
|
3273
|
+
return entries.map((entry) => `${printable(entry.name)} (${entry.command})`).toSorted().join(", ");
|
|
3274
|
+
}
|
|
3275
|
+
function customLines(policies, custom) {
|
|
3276
|
+
const lines = [];
|
|
3277
|
+
let allows = false;
|
|
3278
|
+
let unreadable2 = false;
|
|
3279
|
+
for (const policy of policies) {
|
|
3280
|
+
const table3 = printable(policy.table);
|
|
3281
|
+
if (custom) {
|
|
3282
|
+
const list = custom.get(asciiLower(policy.table)) ?? [];
|
|
3283
|
+
const valid = list.filter((entry) => !entry.invalid);
|
|
3284
|
+
const invalid = list.filter((entry) => entry.invalid);
|
|
3285
|
+
if (valid.length > 0) {
|
|
3286
|
+
allows = true;
|
|
3287
|
+
lines.push(`${table3} custom: ${listed(valid)}`);
|
|
3288
|
+
}
|
|
3289
|
+
if (invalid.length > 0) {
|
|
3290
|
+
unreadable2 = true;
|
|
3291
|
+
lines.push(`${table3} invalid: ${listed(invalid)}`);
|
|
3292
|
+
}
|
|
3293
|
+
continue;
|
|
3294
|
+
}
|
|
3295
|
+
const names = (policy.policies ?? []).filter(
|
|
3296
|
+
(name) => !SET_POLICY_NAMES.has(name)
|
|
3297
|
+
);
|
|
3298
|
+
if (names.length === 0) continue;
|
|
3299
|
+
lines.push(
|
|
3300
|
+
`${table3} also has policies written as SQL: ${names.map(printable).join(", ")}`
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3303
|
+
if (lines.length === 0) return lines;
|
|
3304
|
+
if (!custom) return [...lines, NAMES_ONLY_NOTE];
|
|
3305
|
+
return [
|
|
3306
|
+
...lines,
|
|
3307
|
+
...allows ? [CUSTOM_NOTE] : [],
|
|
3308
|
+
...unreadable2 ? [INVALID_NOTE] : []
|
|
3309
|
+
];
|
|
3310
|
+
}
|
|
3311
|
+
var POLICY_TABLE_HINTS = /* @__PURE__ */ new Set([
|
|
3312
|
+
CUSTOM_NOTE,
|
|
3313
|
+
INVALID_NOTE,
|
|
3314
|
+
NAMES_ONLY_NOTE
|
|
3315
|
+
]);
|
|
3242
3316
|
function conditionText(action, value) {
|
|
3243
3317
|
if (typeof value === "string") return value;
|
|
3244
3318
|
if (isInvalid(value))
|
|
@@ -3254,11 +3328,19 @@ function conditionText(action, value) {
|
|
|
3254
3328
|
const text = parts.join("; ");
|
|
3255
3329
|
return value.role === "authenticated" ? `${text} (signed-in users only)` : text;
|
|
3256
3330
|
}
|
|
3257
|
-
function policyTable(policies) {
|
|
3331
|
+
function policyTable(policies, custom) {
|
|
3258
3332
|
const header = ["Table", ...POLICY_ACTIONS, "Owner column"];
|
|
3333
|
+
const shown = (policy, action) => {
|
|
3334
|
+
const value = policy[action];
|
|
3335
|
+
const sql2 = (custom?.get(asciiLower(policy.table)) ?? []).filter(
|
|
3336
|
+
(entry) => covers(entry, action)
|
|
3337
|
+
);
|
|
3338
|
+
if (sql2.some((entry) => entry.invalid)) return "invalid";
|
|
3339
|
+
return value === "none" && sql2.length > 0 ? "custom" : cellOf(value);
|
|
3340
|
+
};
|
|
3259
3341
|
const rows = policies.map((policy) => [
|
|
3260
3342
|
printable(policy.table),
|
|
3261
|
-
...POLICY_ACTIONS.map((action) =>
|
|
3343
|
+
...POLICY_ACTIONS.map((action) => shown(policy, action)),
|
|
3262
3344
|
policy.ownerColumn === null ? "-" : printable(policy.ownerColumn)
|
|
3263
3345
|
]);
|
|
3264
3346
|
const widths = header.map(
|
|
@@ -3281,19 +3363,22 @@ function policyTable(policies) {
|
|
|
3281
3363
|
}
|
|
3282
3364
|
)
|
|
3283
3365
|
);
|
|
3284
|
-
|
|
3366
|
+
const sql = customLines(policies, custom);
|
|
3367
|
+
const below = sql.length > 0 ? ["", ...sql] : [];
|
|
3368
|
+
if (lines.length === 0) return [...table3, ...below];
|
|
3285
3369
|
const width = Math.max(...lines.map((line) => line.label.length));
|
|
3286
3370
|
return [
|
|
3287
3371
|
...table3,
|
|
3288
3372
|
"",
|
|
3289
|
-
...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`)
|
|
3373
|
+
...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`),
|
|
3374
|
+
...below
|
|
3290
3375
|
];
|
|
3291
3376
|
}
|
|
3292
3377
|
var UNSHOWN = "has characters that cannot be shown here";
|
|
3293
3378
|
function hintsFor(lines, environment) {
|
|
3294
3379
|
if (environment === null) return lines;
|
|
3295
3380
|
return lines.map(
|
|
3296
|
-
(line) => line.includes(UNSHOWN) || line.startsWith("Add one with ") ? forEnvironment(line, environment) : line
|
|
3381
|
+
(line) => line.includes(UNSHOWN) || line.startsWith("Add one with ") || POLICY_TABLE_HINTS.has(line) ? forEnvironment(line, environment) : line
|
|
3297
3382
|
);
|
|
3298
3383
|
}
|
|
3299
3384
|
function statementLine(policy) {
|
|
@@ -3356,9 +3441,13 @@ async function runPolicyRules(context) {
|
|
|
3356
3441
|
const client = await dataClient(context);
|
|
3357
3442
|
const projectId = await context.resolveProjectId();
|
|
3358
3443
|
const all = await client.backend.policies(projectId);
|
|
3359
|
-
const wanted = table3
|
|
3360
|
-
const policies = wanted === void 0 ? all : all.filter((policy) => policy.table
|
|
3361
|
-
const
|
|
3444
|
+
const wanted = table3 === void 0 ? void 0 : asciiLower(table3);
|
|
3445
|
+
const policies = wanted === void 0 ? all : all.filter((policy) => asciiLower(policy.table) === wanted);
|
|
3446
|
+
const custom = context.flags.json || policies.length === 0 ? void 0 : await client.backend.rlsPolicies(projectId, table3 === void 0 ? {} : { table: table3 }).then(customPolicies, (error) => {
|
|
3447
|
+
if (!isNaroError(error)) throw error;
|
|
3448
|
+
return null;
|
|
3449
|
+
});
|
|
3450
|
+
const lines = policies.length > 0 ? [...policyTable(policies, custom), "", DEFAULT_DENY] : [
|
|
3362
3451
|
table3 === void 0 ? "No tables yet." : `No table named ${printable(table3)}.`
|
|
3363
3452
|
];
|
|
3364
3453
|
context.output.result(
|
|
@@ -3493,7 +3582,10 @@ async function runPolicySet(context) {
|
|
|
3493
3582
|
hintsFor(
|
|
3494
3583
|
[
|
|
3495
3584
|
`\u2705 Saved the policy for ${printable(policy.table)}. ${TAKES_EFFECT}`,
|
|
3496
|
-
...policyTable(
|
|
3585
|
+
...policyTable(
|
|
3586
|
+
[policy],
|
|
3587
|
+
named === void 0 ? void 0 : customPolicies(named)
|
|
3588
|
+
),
|
|
3497
3589
|
...named === void 0 || named.length === 0 ? [] : ["", ...policyStatements(named)]
|
|
3498
3590
|
],
|
|
3499
3591
|
dataEnvironment(context.flags)
|
|
@@ -4207,6 +4299,7 @@ function deployTarget(flags) {
|
|
|
4207
4299
|
async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDeploy }) {
|
|
4208
4300
|
const { flags, output } = context;
|
|
4209
4301
|
const target = deployTarget(flags);
|
|
4302
|
+
const flaggedAdapter = flaggedUserAdapter(flags);
|
|
4210
4303
|
const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
|
|
4211
4304
|
if (target.production && !flags.yes && !flags.json && isInteractive()) {
|
|
4212
4305
|
const confirmed = await context.prompter.confirm("Deploy to production?");
|
|
@@ -4220,6 +4313,12 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
|
|
|
4220
4313
|
const account = await accountLabel(context);
|
|
4221
4314
|
output.log(` ${account} \u2192 ${team.slug}
|
|
4222
4315
|
`);
|
|
4316
|
+
const userAdapter = await askUserAdapter(
|
|
4317
|
+
context,
|
|
4318
|
+
flaggedAdapter,
|
|
4319
|
+
link,
|
|
4320
|
+
canPrompt
|
|
4321
|
+
);
|
|
4223
4322
|
const result = await deps.deploy(
|
|
4224
4323
|
context.cwd,
|
|
4225
4324
|
{
|
|
@@ -4246,7 +4345,8 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
|
|
|
4246
4345
|
)
|
|
4247
4346
|
},
|
|
4248
4347
|
token: flags.token,
|
|
4249
|
-
allowLogin: !flags.json && !flags.yes
|
|
4348
|
+
allowLogin: !flags.json && !flags.yes,
|
|
4349
|
+
...userAdapter ? { userAdapter } : {}
|
|
4250
4350
|
},
|
|
4251
4351
|
(message) => output.log(message)
|
|
4252
4352
|
);
|
|
@@ -4290,6 +4390,44 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
|
|
|
4290
4390
|
]
|
|
4291
4391
|
);
|
|
4292
4392
|
}
|
|
4393
|
+
function flaggedUserAdapter(flags) {
|
|
4394
|
+
if (flags.userAdapter === void 0) return void 0;
|
|
4395
|
+
const choice = parseUserAdapter(flags.userAdapter);
|
|
4396
|
+
if (!choice)
|
|
4397
|
+
throw new NaroError(
|
|
4398
|
+
"VALIDATION_FAILED",
|
|
4399
|
+
"--user-adapter takes replace or keep."
|
|
4400
|
+
);
|
|
4401
|
+
return choice;
|
|
4402
|
+
}
|
|
4403
|
+
async function askUserAdapter(context, flagged, link, canPrompt) {
|
|
4404
|
+
const { flags, output } = context;
|
|
4405
|
+
if (flagged) return flagged;
|
|
4406
|
+
const adapter = await detectUserAdapter(context.cwd, flags.root ?? null);
|
|
4407
|
+
if (!adapter) return void 0;
|
|
4408
|
+
let stored = null;
|
|
4409
|
+
if (link?.projectId) {
|
|
4410
|
+
try {
|
|
4411
|
+
stored = (await (await context.client()).projects.get(link.projectId)).userAdapter;
|
|
4412
|
+
} catch {
|
|
4413
|
+
output.log(userAdapterUnreadable(adapter));
|
|
4414
|
+
return void 0;
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
if (stored) {
|
|
4418
|
+
output.log(userAdapterStored(adapter, stored));
|
|
4419
|
+
return void 0;
|
|
4420
|
+
}
|
|
4421
|
+
if (!canPrompt) {
|
|
4422
|
+
output.log(userAdapterUnasked(adapter));
|
|
4423
|
+
return void 0;
|
|
4424
|
+
}
|
|
4425
|
+
const index = await context.prompter.select(
|
|
4426
|
+
userAdapterQuestion(adapter),
|
|
4427
|
+
USER_ADAPTER_OPTIONS.map((option) => option.label)
|
|
4428
|
+
);
|
|
4429
|
+
return USER_ADAPTER_OPTIONS[index]?.value ?? "replace";
|
|
4430
|
+
}
|
|
4293
4431
|
async function resolveDestination(context, link, canPrompt) {
|
|
4294
4432
|
const client = await context.client();
|
|
4295
4433
|
const teams = await client.teams.list();
|
|
@@ -4616,9 +4754,9 @@ async function verifyAll(client, projectId, hostname, auto = false) {
|
|
|
4616
4754
|
return [
|
|
4617
4755
|
await client.domains.verifyByHostname(projectId, hostname, { auto })
|
|
4618
4756
|
];
|
|
4619
|
-
const
|
|
4757
|
+
const listed2 = await client.domains.list(projectId);
|
|
4620
4758
|
const verified = [];
|
|
4621
|
-
for (const domain of
|
|
4759
|
+
for (const domain of listed2)
|
|
4622
4760
|
verified.push(await client.domains.verify(projectId, domain.id, { auto }));
|
|
4623
4761
|
return verified;
|
|
4624
4762
|
}
|
|
@@ -11157,14 +11295,14 @@ async function listAll(storage, bucket, prefix, recursive, search) {
|
|
|
11157
11295
|
truncated = true;
|
|
11158
11296
|
break;
|
|
11159
11297
|
}
|
|
11160
|
-
const
|
|
11298
|
+
const listed2 = await storage.from(bucket).list(folder, {
|
|
11161
11299
|
limit: PAGE,
|
|
11162
11300
|
offset,
|
|
11163
11301
|
...search === "" ? {} : { search }
|
|
11164
11302
|
});
|
|
11165
|
-
if (isOldSearchRefusal(
|
|
11303
|
+
if (isOldSearchRefusal(listed2.error))
|
|
11166
11304
|
throw new NaroError("BACKEND_OUTDATED", STORAGE_OUTDATED_MESSAGE);
|
|
11167
|
-
const page = unwrap(
|
|
11305
|
+
const page = unwrap(listed2);
|
|
11168
11306
|
for (const entry of page) {
|
|
11169
11307
|
const path = folder === "" ? entry.name : `${folder}/${entry.name}`;
|
|
11170
11308
|
if (isFolder(entry)) {
|
|
@@ -11935,7 +12073,7 @@ async function runObservability(context) {
|
|
|
11935
12073
|
}
|
|
11936
12074
|
|
|
11937
12075
|
// src/commands/project-settings.ts
|
|
11938
|
-
var SET_USAGE2 = "Usage: naro project set [--subdomain <name>] [--framework <preset>] [--root <dir>] [--branch <branch>] [--build-command <script>] [--output-dir <dir>] [--vinext on|off]";
|
|
12076
|
+
var SET_USAGE2 = "Usage: naro project set [--subdomain <name>] [--framework <preset>] [--root <dir>] [--branch <branch>] [--build-command <script>] [--output-dir <dir>] [--vinext on|off] [--user-adapter replace|keep]";
|
|
11939
12077
|
var GRANT_USAGE = "Usage: naro project access grant <user-id> --role <project_admin|project_developer|project_viewer>";
|
|
11940
12078
|
function clearable(value) {
|
|
11941
12079
|
return value === "" ? null : value;
|
|
@@ -11957,6 +12095,15 @@ function settingsFrom(context) {
|
|
|
11957
12095
|
throw new NaroError("VALIDATION_FAILED", "--vinext takes on or off.");
|
|
11958
12096
|
patch.vinextEnabled = flags.vinext === "on";
|
|
11959
12097
|
}
|
|
12098
|
+
if (flags.userAdapter !== void 0) {
|
|
12099
|
+
const choice = parseUserAdapter(flags.userAdapter);
|
|
12100
|
+
if (!choice)
|
|
12101
|
+
throw new NaroError(
|
|
12102
|
+
"VALIDATION_FAILED",
|
|
12103
|
+
"--user-adapter takes replace or keep."
|
|
12104
|
+
);
|
|
12105
|
+
patch.userAdapter = choice;
|
|
12106
|
+
}
|
|
11960
12107
|
if (Object.keys(patch).length === 0)
|
|
11961
12108
|
throw new NaroError("VALIDATION_FAILED", SET_USAGE2);
|
|
11962
12109
|
return patch;
|
|
@@ -14004,6 +14151,8 @@ function meterRow(meter, limitsKnown) {
|
|
|
14004
14151
|
const label3 = METRIC_LABELS[meter.metric];
|
|
14005
14152
|
const used = formatCount(meter.used);
|
|
14006
14153
|
if (!limitsKnown) return [label3, used, "\u2014", "\u2014"];
|
|
14154
|
+
if (typeof meter.perProject === "number")
|
|
14155
|
+
return [label3, used, `${formatCount(meter.perProject)} per project`, "\u2014"];
|
|
14007
14156
|
if (meter.limit === null) return [label3, used, "No limit", "\u2014"];
|
|
14008
14157
|
if (meter.limit === 0) return [label3, used, "Not included", "\u2014"];
|
|
14009
14158
|
return [label3, used, formatCount(meter.limit), usedPercent(meter)];
|
|
@@ -14053,6 +14202,53 @@ function usageLines3(report, summary) {
|
|
|
14053
14202
|
);
|
|
14054
14203
|
return lines;
|
|
14055
14204
|
}
|
|
14205
|
+
function tenths(value) {
|
|
14206
|
+
return Math.round(value * 10) / 10;
|
|
14207
|
+
}
|
|
14208
|
+
function cpuText(ms) {
|
|
14209
|
+
const whole = Math.round(ms);
|
|
14210
|
+
if (whole < 1e3) return `${String(whole)} ms`;
|
|
14211
|
+
const seconds = tenths(ms / 1e3);
|
|
14212
|
+
if (seconds < 60) return `${seconds.toFixed(1)} s`;
|
|
14213
|
+
const minutes = tenths(ms / 6e4);
|
|
14214
|
+
if (minutes < 60) return `${minutes.toFixed(1)} min`;
|
|
14215
|
+
return `${tenths(ms / 36e5).toFixed(1)} h`;
|
|
14216
|
+
}
|
|
14217
|
+
function environmentName(environment) {
|
|
14218
|
+
switch (environment.kind) {
|
|
14219
|
+
case "production":
|
|
14220
|
+
return "production";
|
|
14221
|
+
case "preview":
|
|
14222
|
+
return "preview";
|
|
14223
|
+
case "environment":
|
|
14224
|
+
return printable(environment.label);
|
|
14225
|
+
case "former":
|
|
14226
|
+
return `earlier name ${printable(environment.workerName)}`;
|
|
14227
|
+
}
|
|
14228
|
+
}
|
|
14229
|
+
function hostingLines(hosting) {
|
|
14230
|
+
if (!hosting || hosting.projects.length === 0) return [];
|
|
14231
|
+
const rows = [["CPU", "Invocations", "Project / environment"]];
|
|
14232
|
+
for (const project of hosting.projects) {
|
|
14233
|
+
rows.push([
|
|
14234
|
+
cpuText(project.cpuMs),
|
|
14235
|
+
formatCount(project.invocations),
|
|
14236
|
+
project.name === null ? `Not in this team ${printable(project.projectId)}` : printable(project.name)
|
|
14237
|
+
]);
|
|
14238
|
+
for (const environment of project.environments)
|
|
14239
|
+
rows.push([
|
|
14240
|
+
cpuText(environment.cpuMs),
|
|
14241
|
+
formatCount(environment.invocations),
|
|
14242
|
+
` ${environmentName(environment)}`
|
|
14243
|
+
]);
|
|
14244
|
+
}
|
|
14245
|
+
return [
|
|
14246
|
+
"",
|
|
14247
|
+
"Compute (CPU) \xB7 observed, not billed \xB7 sampled estimate",
|
|
14248
|
+
...table(rows),
|
|
14249
|
+
`Total: ${cpuText(hosting.totals.cpuMs)} of CPU over ${formatCount(hosting.totals.invocations)} invocations`
|
|
14250
|
+
];
|
|
14251
|
+
}
|
|
14056
14252
|
|
|
14057
14253
|
// src/commands/usage.ts
|
|
14058
14254
|
async function runUsage(context) {
|
|
@@ -14060,12 +14256,26 @@ async function runUsage(context) {
|
|
|
14060
14256
|
const teams = await client.teams.list();
|
|
14061
14257
|
const team = await resolveTeam(context, teams, { intent: "read" });
|
|
14062
14258
|
const month = context.flags.month;
|
|
14063
|
-
const
|
|
14064
|
-
|
|
14065
|
-
month !== void 0 ? { month } : {}
|
|
14066
|
-
);
|
|
14259
|
+
const query = month !== void 0 ? { month } : {};
|
|
14260
|
+
const report = await client.teams.usage(team.slug, query);
|
|
14067
14261
|
const summary = summarizeTeamUsage(report, usageMonthOf(/* @__PURE__ */ new Date()));
|
|
14068
|
-
|
|
14262
|
+
let hosting = null;
|
|
14263
|
+
let hostingError;
|
|
14264
|
+
try {
|
|
14265
|
+
hosting = await client.teams.usageHosting(team.slug, query);
|
|
14266
|
+
} catch (error) {
|
|
14267
|
+
hostingError = {
|
|
14268
|
+
code: error instanceof NaroError ? error.code : "UNKNOWN",
|
|
14269
|
+
message: error instanceof Error ? error.message : String(error)
|
|
14270
|
+
};
|
|
14271
|
+
context.output.warn(
|
|
14272
|
+
`Could not read compute usage: ${hostingError.message}`
|
|
14273
|
+
);
|
|
14274
|
+
}
|
|
14275
|
+
context.output.result(
|
|
14276
|
+
hostingError ? { ...report, summary, hosting, hostingError } : { ...report, summary, hosting },
|
|
14277
|
+
[...usageLines3(report, summary), ...hostingLines(hosting)]
|
|
14278
|
+
);
|
|
14069
14279
|
}
|
|
14070
14280
|
|
|
14071
14281
|
// src/router.ts
|
|
@@ -14098,6 +14308,7 @@ Usage:
|
|
|
14098
14308
|
naro project delete Delete the project and everything under it: type its name to confirm
|
|
14099
14309
|
naro project set [--subdomain <name>] [--framework <preset>] [--root <dir>] [--branch <branch>]
|
|
14100
14310
|
[--build-command <script>] [--output-dir <dir>] [--vinext on|off]
|
|
14311
|
+
[--user-adapter replace|keep]
|
|
14101
14312
|
Change the General settings; an empty value clears one. A new subdomain
|
|
14102
14313
|
moves the project's address at once (asks first; --yes in CI)
|
|
14103
14314
|
naro project check <subdomain> Whether a subdomain is free
|
|
@@ -14107,7 +14318,7 @@ Usage:
|
|
|
14107
14318
|
Take a contributor's project role away (asks first)
|
|
14108
14319
|
[--project <id> --yes] (in CI both; --yes alone is refused so the folder cannot choose the project)
|
|
14109
14320
|
naro usage [--month YYYY-MM] [--team <slug>]
|
|
14110
|
-
Team usage: requests, builds and custom domains against the plan, then by project
|
|
14321
|
+
Team usage: requests, builds and custom domains against the plan, then by project, then CPU by project and environment
|
|
14111
14322
|
naro billing [--team <slug>] The team's plan, subscription and this month's overage; the plan changes in the dashboard
|
|
14112
14323
|
naro dashboard [<capability>] The dashboard URL for what the CLI leaves there (payments, GitHub App, \u2026); no argument lists them
|
|
14113
14324
|
|
|
@@ -14283,7 +14494,7 @@ Usage:
|
|
|
14283
14494
|
naro backend users delete <id|username|email>
|
|
14284
14495
|
Delete a user and end its sessions (asks first; --yes to skip)
|
|
14285
14496
|
naro policy list [--table <t>] Row-level security policies as CREATE POLICY (no policy = deny)
|
|
14286
|
-
naro policy rules [--table <t>] Each table's policies as rules (public, authenticated, owner, none) and conditions
|
|
14497
|
+
naro policy rules [--table <t>] Each table's policies as rules (public, authenticated, owner, none, custom, invalid) and conditions
|
|
14287
14498
|
naro policy create "CREATE POLICY <name> ON <table> \u2026" | --file <policies.sql>
|
|
14288
14499
|
Postgres's CREATE POLICY: AS PERMISSIVE|RESTRICTIVE,
|
|
14289
14500
|
FOR ALL|SELECT|INSERT|UPDATE|DELETE, TO anon|authenticated|public,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type UserAdapterChoice = "replace" | "keep";
|
|
2
|
+
export interface DetectedUserAdapter {
|
|
3
|
+
/** The active next.config, relative to the app directory. */
|
|
4
|
+
file: string;
|
|
5
|
+
key: "adapterPath" | "experimental.adapterPath";
|
|
6
|
+
}
|
|
7
|
+
/** The next.config Next would load sets adapterPath, read the way Naro's builders read it. */
|
|
8
|
+
export declare function detectUserAdapter(directory: string, rootDirectory: string | null): Promise<DetectedUserAdapter | null>;
|
|
9
|
+
export declare function parseUserAdapter(value: unknown): UserAdapterChoice | null;
|
|
10
|
+
export declare const USER_ADAPTER_OPTIONS: ReadonlyArray<{
|
|
11
|
+
value: UserAdapterChoice;
|
|
12
|
+
label: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function userAdapterQuestion(adapter: DetectedUserAdapter): string;
|
|
15
|
+
/** One line on what a choice means for this deploy (the local build has no Naro adapter, spec S2 D8). */
|
|
16
|
+
export declare function userAdapterOutcome(choice: UserAdapterChoice): string;
|
|
17
|
+
/** The line for a project that already chose. */
|
|
18
|
+
export declare function userAdapterStored(adapter: DetectedUserAdapter, choice: UserAdapterChoice): string;
|
|
19
|
+
/** The warning when nobody chose and nobody can be asked. */
|
|
20
|
+
export declare function userAdapterUnasked(adapter: DetectedUserAdapter): string;
|
|
21
|
+
/** Where each surface changes the stored choice. */
|
|
22
|
+
declare const CHOOSE_USER_ADAPTER: {
|
|
23
|
+
readonly cli: "naro project set --user-adapter replace|keep";
|
|
24
|
+
readonly mcp: "update_project with userAdapter replace|keep";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The warning when the project's stored choice could not be read. It may
|
|
28
|
+
* already say keep, so this line must not claim replace the way
|
|
29
|
+
* userAdapterUnasked does.
|
|
30
|
+
*/
|
|
31
|
+
export declare function userAdapterUnreadable(adapter: DetectedUserAdapter, surface?: keyof typeof CHOOSE_USER_ADAPTER): string;
|
|
32
|
+
export {};
|
package/dist/working-tree.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ export interface WorkingTreeCopy {
|
|
|
49
49
|
* edit to a committed `.env`, one hidden behind `skip-worktree`, one only
|
|
50
50
|
* staged, and an untracked `.env.local` all stay on the machine. `next build`
|
|
51
51
|
* reads those files on its own and inlines NEXT_PUBLIC_* into the bundle, and
|
|
52
|
-
*
|
|
52
|
+
* the bundler inlines every value they hold into the Worker, so a file copied
|
|
53
53
|
* here would ship values the developer declined to send. The env consent
|
|
54
54
|
* prompt is the only way local values reach a build (known-issues #7,
|
|
55
55
|
* CLI-DEPLOY-011).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"naro": "./dist/index.js"
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@cloudflare/unenv-preset": "2.16.2",
|
|
26
|
-
"@opennextjs/cloudflare": "1.20.12",
|
|
27
26
|
"@supabase/supabase-js": "2.112.3",
|
|
28
27
|
"@vercel/frameworks": "3.34.0",
|
|
29
28
|
"esbuild": "0.28.1",
|
|
@@ -38,13 +37,13 @@
|
|
|
38
37
|
"typescript": "7.0.2",
|
|
39
38
|
"vitest": "3.2.7",
|
|
40
39
|
"naro-js": "0.9.0",
|
|
41
|
-
"@next-deploy/config": "0.1.0",
|
|
42
|
-
"@next-deploy/build": "0.1.0",
|
|
43
40
|
"@next-deploy/core": "0.1.0",
|
|
44
41
|
"@next-deploy/data-core": "0.1.0",
|
|
45
|
-
"@next-deploy/functions-bundler": "0.1.0",
|
|
46
42
|
"@next-deploy/functions-runtime": "0.1.0",
|
|
47
|
-
"@next-deploy/sdk": "0.1.0"
|
|
43
|
+
"@next-deploy/sdk": "0.1.0",
|
|
44
|
+
"@next-deploy/config": "0.1.0",
|
|
45
|
+
"@next-deploy/functions-bundler": "0.1.0",
|
|
46
|
+
"@next-deploy/build": "0.1.0"
|
|
48
47
|
},
|
|
49
48
|
"scripts": {
|
|
50
49
|
"build": "tsup && pnpm build:types",
|