naro-cli 0.3.0 → 0.4.1
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 +13 -5
- package/dist/{chunk-B6F7QIW7.js → chunk-YLFLDW2H.js} +302 -33
- package/dist/deploy.d.ts +7 -0
- package/dist/deploy.js +2 -2
- package/dist/env-consent.d.ts +18 -0
- package/dist/index.js +183 -15
- package/dist/output.d.ts +29 -0
- package/dist/prompt.d.ts +17 -0
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -13,11 +13,19 @@ naro deploy --prod # production deployment
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
The first `naro deploy` creates the project (named after `package.json`),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
links the directory via `.naro/project.json` (gitignored automatically) and
|
|
17
|
+
prints the live URL. No Cloudflare credentials are ever needed.
|
|
18
|
+
|
|
19
|
+
Local `.env` files are never uploaded on their own. Every deploy scans your
|
|
20
|
+
source for the env vars it references (`process.env.X`, `import.meta.env.X`,
|
|
21
|
+
plus anything listed in `.env.example`), compares that with the project, and
|
|
22
|
+
asks before opening a dotenv file to fill what is missing. Decline and it
|
|
23
|
+
prints the `naro env add` commands instead. With `--yes`, `--json` or outside
|
|
24
|
+
a TTY it never asks — it just lists what is missing and deploys.
|
|
25
|
+
|
|
26
|
+
Values you register are encrypted at rest, and `naro env pull` /
|
|
27
|
+
`naro env list --reveal` can read them back — your account credentials are
|
|
28
|
+
effectively your env credentials.
|
|
21
29
|
|
|
22
30
|
## Commands
|
|
23
31
|
|
|
@@ -6,10 +6,10 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/deploy.ts
|
|
9
|
-
import { existsSync as
|
|
9
|
+
import { existsSync as existsSync6 } from "fs";
|
|
10
10
|
import { cp, mkdtemp as mkdtemp2, readFile as readFile7, rm as rm2 } from "fs/promises";
|
|
11
11
|
import { tmpdir as tmpdir2 } from "os";
|
|
12
|
-
import { basename, dirname, join as
|
|
12
|
+
import { basename, dirname, join as join15 } from "path";
|
|
13
13
|
import { fileURLToPath } from "url";
|
|
14
14
|
|
|
15
15
|
// ../../packages/build/src/artifact-upload.ts
|
|
@@ -186,6 +186,27 @@ var DeploymentError = class extends Error {
|
|
|
186
186
|
}
|
|
187
187
|
};
|
|
188
188
|
|
|
189
|
+
// ../../packages/core/src/git-remote.ts
|
|
190
|
+
var SCP_LIKE = /^[^@\s/]+@([^:\s/]+):(.+)$/;
|
|
191
|
+
function parseGitHubRemote(url2) {
|
|
192
|
+
const trimmed = url2.trim();
|
|
193
|
+
if (trimmed === "") return null;
|
|
194
|
+
const scp = SCP_LIKE.exec(trimmed);
|
|
195
|
+
const path = scp ? scp[2] ?? null : pathnameOf(trimmed);
|
|
196
|
+
if (path === null) return null;
|
|
197
|
+
const segments = path.replace(/\.git$/, "").split("/").filter((segment) => segment !== "");
|
|
198
|
+
if (segments.length !== 2) return null;
|
|
199
|
+
const [owner, repo] = segments;
|
|
200
|
+
return { owner, repo, fullName: `${owner}/${repo}` };
|
|
201
|
+
}
|
|
202
|
+
function pathnameOf(url2) {
|
|
203
|
+
try {
|
|
204
|
+
return new URL(url2).pathname;
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
189
210
|
// ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
190
211
|
var external_exports = {};
|
|
191
212
|
__export(external_exports, {
|
|
@@ -14950,7 +14971,10 @@ export default defineCloudflareConfig({});
|
|
|
14950
14971
|
)}
|
|
14951
14972
|
`
|
|
14952
14973
|
);
|
|
14953
|
-
const packageManagerConfigPath = await createPackageManagerConfig(
|
|
14974
|
+
const packageManagerConfigPath = await createPackageManagerConfig(
|
|
14975
|
+
appRoot,
|
|
14976
|
+
writer
|
|
14977
|
+
);
|
|
14954
14978
|
return {
|
|
14955
14979
|
openNextConfigPath,
|
|
14956
14980
|
wranglerConfigPath,
|
|
@@ -15735,7 +15759,7 @@ var LocalBuildExecutor = class {
|
|
|
15735
15759
|
args: ["run", resolved.buildScript],
|
|
15736
15760
|
cwd: appRoot,
|
|
15737
15761
|
env: {
|
|
15738
|
-
...input.env
|
|
15762
|
+
...input.env,
|
|
15739
15763
|
...isNext ? { NEXT_PRIVATE_STANDALONE: "true" } : {},
|
|
15740
15764
|
NPM_CONFIG_USERCONFIG: packageManagerConfigPath
|
|
15741
15765
|
},
|
|
@@ -15865,6 +15889,8 @@ async function errorFromResponse(response, context) {
|
|
|
15865
15889
|
const code = serverCode ?? (response.status === 401 ? "UNAUTHORIZED" : response.status === 404 ? "NOT_FOUND" : response.status === 429 ? "RATE_LIMITED" : "INTERNAL_ERROR");
|
|
15866
15890
|
const details = {};
|
|
15867
15891
|
if (body?.error?.fields !== void 0) details.fields = body.error.fields;
|
|
15892
|
+
if (typeof body?.error?.installUrl === "string")
|
|
15893
|
+
details.installUrl = body.error.installUrl;
|
|
15868
15894
|
return new NaroError(
|
|
15869
15895
|
code,
|
|
15870
15896
|
serverMessage ?? `${context} failed: the control plane returned ${String(response.status)}.`,
|
|
@@ -16189,6 +16215,7 @@ var NaroClient = class {
|
|
|
16189
16215
|
doFetch;
|
|
16190
16216
|
teams;
|
|
16191
16217
|
projects;
|
|
16218
|
+
github;
|
|
16192
16219
|
deployments;
|
|
16193
16220
|
env;
|
|
16194
16221
|
domains;
|
|
@@ -16199,6 +16226,7 @@ var NaroClient = class {
|
|
|
16199
16226
|
this.doFetch = options.fetch ?? fetch;
|
|
16200
16227
|
this.teams = new TeamsApi(this);
|
|
16201
16228
|
this.projects = new ProjectsApi(this);
|
|
16229
|
+
this.github = new GithubApi(this);
|
|
16202
16230
|
this.deployments = new DeploymentsApi(this);
|
|
16203
16231
|
this.env = new EnvApi(this);
|
|
16204
16232
|
this.domains = new DomainsApi(this);
|
|
@@ -16257,6 +16285,51 @@ var ProjectsApi = class {
|
|
|
16257
16285
|
);
|
|
16258
16286
|
return projectInfo(body.project);
|
|
16259
16287
|
}
|
|
16288
|
+
/**
|
|
16289
|
+
* Connects a project to a GitHub repository, turning `git push` into a
|
|
16290
|
+
* deployment.
|
|
16291
|
+
*
|
|
16292
|
+
* "The App is not installed on that account" is a normal branch here, not a
|
|
16293
|
+
* failure: the CLI opens a browser and polls, while MCP hands the URL back
|
|
16294
|
+
* to the agent. So it is returned rather than thrown.
|
|
16295
|
+
*/
|
|
16296
|
+
async connectGit(projectId, fullName) {
|
|
16297
|
+
try {
|
|
16298
|
+
const body = await this.client.request(
|
|
16299
|
+
`/api/projects/${encodeURIComponent(projectId)}/git`,
|
|
16300
|
+
{ method: "POST", body: { fullName }, context: "Connect repository" }
|
|
16301
|
+
);
|
|
16302
|
+
return { ok: true, project: projectInfo(body.project) };
|
|
16303
|
+
} catch (error51) {
|
|
16304
|
+
if (isNaroError(error51) && error51.code === "REPOSITORY_NOT_ACCESSIBLE") {
|
|
16305
|
+
const installUrl = error51.details?.installUrl;
|
|
16306
|
+
return {
|
|
16307
|
+
ok: false,
|
|
16308
|
+
installUrl: typeof installUrl === "string" ? installUrl : null
|
|
16309
|
+
};
|
|
16310
|
+
}
|
|
16311
|
+
throw error51;
|
|
16312
|
+
}
|
|
16313
|
+
}
|
|
16314
|
+
async disconnectGit(projectId) {
|
|
16315
|
+
const body = await this.client.request(
|
|
16316
|
+
`/api/projects/${encodeURIComponent(projectId)}/git`,
|
|
16317
|
+
{ method: "DELETE", context: "Disconnect repository" }
|
|
16318
|
+
);
|
|
16319
|
+
return projectInfo(body.project);
|
|
16320
|
+
}
|
|
16321
|
+
};
|
|
16322
|
+
var GithubApi = class {
|
|
16323
|
+
constructor(client) {
|
|
16324
|
+
this.client = client;
|
|
16325
|
+
}
|
|
16326
|
+
/** Repositories every installation attached to the team can reach. */
|
|
16327
|
+
async listRepositories(teamSlug) {
|
|
16328
|
+
const body = await this.client.request(`/api/github/repositories?team=${encodeURIComponent(teamSlug)}`, {
|
|
16329
|
+
context: "List repositories"
|
|
16330
|
+
});
|
|
16331
|
+
return body.repositories;
|
|
16332
|
+
}
|
|
16260
16333
|
};
|
|
16261
16334
|
var DeploymentsApi = class {
|
|
16262
16335
|
constructor(client) {
|
|
@@ -16517,13 +16590,118 @@ function serializeDotenv(vars) {
|
|
|
16517
16590
|
`;
|
|
16518
16591
|
}
|
|
16519
16592
|
|
|
16520
|
-
// ../../packages/sdk/src/
|
|
16521
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
16593
|
+
// ../../packages/sdk/src/env-scan.ts
|
|
16594
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
|
|
16522
16595
|
import { join as join11 } from "path";
|
|
16596
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
16597
|
+
".ts",
|
|
16598
|
+
".tsx",
|
|
16599
|
+
".js",
|
|
16600
|
+
".jsx",
|
|
16601
|
+
".mjs",
|
|
16602
|
+
".cjs",
|
|
16603
|
+
".mts",
|
|
16604
|
+
".cts",
|
|
16605
|
+
".vue",
|
|
16606
|
+
".svelte",
|
|
16607
|
+
".astro"
|
|
16608
|
+
]);
|
|
16609
|
+
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
16610
|
+
"node_modules",
|
|
16611
|
+
"dist",
|
|
16612
|
+
"build",
|
|
16613
|
+
"out",
|
|
16614
|
+
"coverage",
|
|
16615
|
+
"tmp"
|
|
16616
|
+
]);
|
|
16617
|
+
var EXAMPLE_FILES = [".env.example", ".env.sample", ".env.template"];
|
|
16618
|
+
var IGNORED_KEYS = /* @__PURE__ */ new Set([
|
|
16619
|
+
"NODE_ENV",
|
|
16620
|
+
"PORT",
|
|
16621
|
+
"CI",
|
|
16622
|
+
"HOME",
|
|
16623
|
+
"PATH",
|
|
16624
|
+
"PWD",
|
|
16625
|
+
"TZ",
|
|
16626
|
+
"NEXT_RUNTIME",
|
|
16627
|
+
"MODE",
|
|
16628
|
+
"BASE_URL",
|
|
16629
|
+
"DEV",
|
|
16630
|
+
"PROD",
|
|
16631
|
+
"SSR"
|
|
16632
|
+
]);
|
|
16633
|
+
var MAX_FILES = 5e3;
|
|
16634
|
+
var MAX_FILE_BYTES2 = 1024 * 1024;
|
|
16635
|
+
var REFERENCE = /(?:process|import\.meta)\.env(?:\.([A-Za-z_][A-Za-z0-9_]*)|\[\s*["'`]([A-Za-z_][A-Za-z0-9_]*)["'`]\s*\])/g;
|
|
16636
|
+
function ignored(key) {
|
|
16637
|
+
return IGNORED_KEYS.has(key) || key.startsWith("npm_");
|
|
16638
|
+
}
|
|
16639
|
+
function collect(dir, out) {
|
|
16640
|
+
if (out.length >= MAX_FILES) return;
|
|
16641
|
+
let entries;
|
|
16642
|
+
try {
|
|
16643
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
16644
|
+
} catch {
|
|
16645
|
+
return;
|
|
16646
|
+
}
|
|
16647
|
+
for (const entry of entries) {
|
|
16648
|
+
if (out.length >= MAX_FILES) return;
|
|
16649
|
+
if (entry.isSymbolicLink()) continue;
|
|
16650
|
+
const path = join11(dir, entry.name);
|
|
16651
|
+
if (entry.isDirectory()) {
|
|
16652
|
+
if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name))
|
|
16653
|
+
continue;
|
|
16654
|
+
collect(path, out);
|
|
16655
|
+
continue;
|
|
16656
|
+
}
|
|
16657
|
+
if (!entry.isFile()) continue;
|
|
16658
|
+
const dot = entry.name.lastIndexOf(".");
|
|
16659
|
+
if (dot < 0 || !SOURCE_EXTENSIONS.has(entry.name.slice(dot))) continue;
|
|
16660
|
+
out.push(path);
|
|
16661
|
+
}
|
|
16662
|
+
}
|
|
16663
|
+
function scanEnvReferences(dir) {
|
|
16664
|
+
const found = /* @__PURE__ */ new Map();
|
|
16665
|
+
const files = [];
|
|
16666
|
+
collect(dir, files);
|
|
16667
|
+
for (const file2 of files) {
|
|
16668
|
+
let text;
|
|
16669
|
+
try {
|
|
16670
|
+
if (statSync(file2).size > MAX_FILE_BYTES2) continue;
|
|
16671
|
+
text = readFileSync3(file2, "utf8");
|
|
16672
|
+
} catch {
|
|
16673
|
+
continue;
|
|
16674
|
+
}
|
|
16675
|
+
for (const match of text.matchAll(REFERENCE)) {
|
|
16676
|
+
const key = match[1] ?? match[2];
|
|
16677
|
+
if (!key || ignored(key)) continue;
|
|
16678
|
+
found.set(key, "code");
|
|
16679
|
+
}
|
|
16680
|
+
}
|
|
16681
|
+
for (const name of EXAMPLE_FILES) {
|
|
16682
|
+
const path = join11(dir, name);
|
|
16683
|
+
if (!existsSync2(path)) continue;
|
|
16684
|
+
let parsed;
|
|
16685
|
+
try {
|
|
16686
|
+
parsed = parseDotenv(readFileSync3(path, "utf8"));
|
|
16687
|
+
} catch {
|
|
16688
|
+
continue;
|
|
16689
|
+
}
|
|
16690
|
+
for (const key of Object.keys(parsed)) {
|
|
16691
|
+
if (ignored(key) || found.has(key)) continue;
|
|
16692
|
+
found.set(key, "example");
|
|
16693
|
+
}
|
|
16694
|
+
}
|
|
16695
|
+
return [...found].map(([key, source]) => ({ key, source })).sort((a, b) => a.key.localeCompare(b.key));
|
|
16696
|
+
}
|
|
16697
|
+
|
|
16698
|
+
// ../../packages/sdk/src/framework.ts
|
|
16699
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
16700
|
+
import { join as join12 } from "path";
|
|
16523
16701
|
function readPackageJson2(dir) {
|
|
16524
16702
|
try {
|
|
16525
16703
|
return JSON.parse(
|
|
16526
|
-
|
|
16704
|
+
readFileSync4(join12(dir, "package.json"), "utf8")
|
|
16527
16705
|
);
|
|
16528
16706
|
} catch {
|
|
16529
16707
|
return null;
|
|
@@ -16534,7 +16712,7 @@ function hasDependency(pkg, name) {
|
|
|
16534
16712
|
}
|
|
16535
16713
|
function hasConfig(dir, base2, evidence) {
|
|
16536
16714
|
for (const ext of ["js", "mjs", "cjs", "ts", "mts"]) {
|
|
16537
|
-
if (
|
|
16715
|
+
if (existsSync3(join12(dir, `${base2}.${ext}`))) {
|
|
16538
16716
|
evidence.push(`${base2}.${ext}`);
|
|
16539
16717
|
return true;
|
|
16540
16718
|
}
|
|
@@ -16546,8 +16724,8 @@ function detectFramework2(dir) {
|
|
|
16546
16724
|
const pkg = readPackageJson2(dir);
|
|
16547
16725
|
if (hasDependency(pkg, "next") || hasConfig(dir, "next.config", evidence)) {
|
|
16548
16726
|
if (hasDependency(pkg, "next")) evidence.push("package.json: next");
|
|
16549
|
-
if (
|
|
16550
|
-
if (
|
|
16727
|
+
if (existsSync3(join12(dir, "app"))) evidence.push("app/");
|
|
16728
|
+
if (existsSync3(join12(dir, "pages"))) evidence.push("pages/");
|
|
16551
16729
|
return {
|
|
16552
16730
|
framework: "nextjs",
|
|
16553
16731
|
name: "Next.js",
|
|
@@ -16601,7 +16779,7 @@ function detectFramework2(dir) {
|
|
|
16601
16779
|
deployable: true
|
|
16602
16780
|
};
|
|
16603
16781
|
}
|
|
16604
|
-
if (!pkg &&
|
|
16782
|
+
if (!pkg && existsSync3(join12(dir, "index.html"))) {
|
|
16605
16783
|
evidence.push("index.html (no package.json)");
|
|
16606
16784
|
return {
|
|
16607
16785
|
framework: "static",
|
|
@@ -16624,28 +16802,29 @@ function detectFramework2(dir) {
|
|
|
16624
16802
|
|
|
16625
16803
|
// ../../packages/sdk/src/link.ts
|
|
16626
16804
|
import {
|
|
16627
|
-
existsSync as
|
|
16805
|
+
existsSync as existsSync4,
|
|
16628
16806
|
mkdirSync as mkdirSync2,
|
|
16629
|
-
readFileSync as
|
|
16807
|
+
readFileSync as readFileSync5,
|
|
16630
16808
|
rmSync as rmSync2,
|
|
16631
16809
|
writeFileSync as writeFileSync2
|
|
16632
16810
|
} from "fs";
|
|
16633
|
-
import { join as
|
|
16811
|
+
import { join as join13 } from "path";
|
|
16634
16812
|
function linkDir(dir) {
|
|
16635
|
-
return
|
|
16813
|
+
return join13(dir, ".naro");
|
|
16636
16814
|
}
|
|
16637
16815
|
function linkFile(dir) {
|
|
16638
|
-
return
|
|
16816
|
+
return join13(linkDir(dir), "project.json");
|
|
16639
16817
|
}
|
|
16640
16818
|
function readProjectLink(dir) {
|
|
16641
16819
|
try {
|
|
16642
|
-
const parsed = JSON.parse(
|
|
16820
|
+
const parsed = JSON.parse(readFileSync5(linkFile(dir), "utf8"));
|
|
16643
16821
|
if (typeof parsed.projectId !== "string" || parsed.projectId.length === 0)
|
|
16644
16822
|
return null;
|
|
16645
16823
|
return {
|
|
16646
16824
|
projectId: parsed.projectId,
|
|
16647
16825
|
...typeof parsed.teamSlug === "string" ? { teamSlug: parsed.teamSlug } : {},
|
|
16648
|
-
...typeof parsed.projectName === "string" ? { projectName: parsed.projectName } : {}
|
|
16826
|
+
...typeof parsed.projectName === "string" ? { projectName: parsed.projectName } : {},
|
|
16827
|
+
...parsed.autoDeployDeclined === true ? { autoDeployDeclined: true } : {}
|
|
16649
16828
|
};
|
|
16650
16829
|
} catch {
|
|
16651
16830
|
return null;
|
|
@@ -16656,20 +16835,20 @@ function writeProjectLink(dir, link) {
|
|
|
16656
16835
|
writeFileSync2(linkFile(dir), `${JSON.stringify(link, null, 2)}
|
|
16657
16836
|
`);
|
|
16658
16837
|
writeFileSync2(
|
|
16659
|
-
|
|
16838
|
+
join13(linkDir(dir), "README.txt"),
|
|
16660
16839
|
"This directory links the project to Naro and should not be committed.\n"
|
|
16661
16840
|
);
|
|
16662
16841
|
}
|
|
16663
16842
|
function removeProjectLink(dir) {
|
|
16664
|
-
if (!
|
|
16843
|
+
if (!existsSync4(linkDir(dir))) return false;
|
|
16665
16844
|
rmSync2(linkDir(dir), { recursive: true, force: true });
|
|
16666
16845
|
return true;
|
|
16667
16846
|
}
|
|
16668
16847
|
function ensureGitignoreHasNaro(dir) {
|
|
16669
|
-
const gitignorePath =
|
|
16670
|
-
const hasGitignore =
|
|
16671
|
-
if (!hasGitignore && !
|
|
16672
|
-
const current = hasGitignore ?
|
|
16848
|
+
const gitignorePath = join13(dir, ".gitignore");
|
|
16849
|
+
const hasGitignore = existsSync4(gitignorePath);
|
|
16850
|
+
if (!hasGitignore && !existsSync4(join13(dir, ".git"))) return false;
|
|
16851
|
+
const current = hasGitignore ? readFileSync5(gitignorePath, "utf8") : "";
|
|
16673
16852
|
const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".naro" || line === ".naro/" || line === "/.naro");
|
|
16674
16853
|
if (covered) return false;
|
|
16675
16854
|
const suffix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
|
|
@@ -16678,6 +16857,67 @@ function ensureGitignoreHasNaro(dir) {
|
|
|
16678
16857
|
return true;
|
|
16679
16858
|
}
|
|
16680
16859
|
|
|
16860
|
+
// src/env-consent.ts
|
|
16861
|
+
import { existsSync as existsSync5 } from "fs";
|
|
16862
|
+
import { join as join14 } from "path";
|
|
16863
|
+
var DOTENV_FILES = [".env.local", ".env.production", ".env"];
|
|
16864
|
+
var PUBLIC_PREFIXES = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_"];
|
|
16865
|
+
function maskEnvValue(key, value) {
|
|
16866
|
+
if (PUBLIC_PREFIXES.some((prefix) => key.startsWith(prefix)))
|
|
16867
|
+
return value.length > 60 ? `${value.slice(0, 60)}\u2026` : value;
|
|
16868
|
+
return value.length <= 8 ? "****" : `${value.slice(0, 4)}****`;
|
|
16869
|
+
}
|
|
16870
|
+
function findDotenvFile(dir) {
|
|
16871
|
+
for (const name of DOTENV_FILES) if (existsSync5(join14(dir, name))) return name;
|
|
16872
|
+
return null;
|
|
16873
|
+
}
|
|
16874
|
+
function envAddGuidance(keys) {
|
|
16875
|
+
return [
|
|
16876
|
+
" Set them yourself:",
|
|
16877
|
+
...keys.map((key) => ` npx naro-cli env add ${key}`),
|
|
16878
|
+
" (values come from a hidden prompt or stdin, never from argv)"
|
|
16879
|
+
];
|
|
16880
|
+
}
|
|
16881
|
+
async function promptForMissingEnv(ctx, keys) {
|
|
16882
|
+
const guide = (remaining2) => {
|
|
16883
|
+
for (const line of envAddGuidance(remaining2)) ctx.output.log(line);
|
|
16884
|
+
};
|
|
16885
|
+
const file2 = findDotenvFile(ctx.cwd);
|
|
16886
|
+
if (!file2) {
|
|
16887
|
+
guide(keys);
|
|
16888
|
+
return {};
|
|
16889
|
+
}
|
|
16890
|
+
const consented = await ctx.prompter.confirm(
|
|
16891
|
+
`Found ${file2}. Read it to fill these ${String(keys.length)} value(s)?`
|
|
16892
|
+
);
|
|
16893
|
+
if (!consented) {
|
|
16894
|
+
ctx.output.log(`\u25B8 skipped reading ${file2}`);
|
|
16895
|
+
guide(keys);
|
|
16896
|
+
return {};
|
|
16897
|
+
}
|
|
16898
|
+
const local = (ctx.readEnv ?? readProjectEnv)(ctx.cwd);
|
|
16899
|
+
const collected = {};
|
|
16900
|
+
for (const key of keys) {
|
|
16901
|
+
const value = local[key];
|
|
16902
|
+
if (value !== void 0 && value !== "") collected[key] = value;
|
|
16903
|
+
}
|
|
16904
|
+
const names = Object.keys(collected);
|
|
16905
|
+
if (names.length === 0) {
|
|
16906
|
+
ctx.output.log(`\u25B8 no matching values in ${file2}`);
|
|
16907
|
+
guide(keys);
|
|
16908
|
+
return {};
|
|
16909
|
+
}
|
|
16910
|
+
const width = Math.max(...names.map((name) => name.length));
|
|
16911
|
+
for (const key of names)
|
|
16912
|
+
ctx.output.log(
|
|
16913
|
+
` ${key.padEnd(width)} ${maskEnvValue(key, collected[key] ?? "")}`
|
|
16914
|
+
);
|
|
16915
|
+
ctx.output.log(`\u25B8 adding ${String(names.length)} env var(s)`);
|
|
16916
|
+
const remaining = keys.filter((key) => !(key in collected));
|
|
16917
|
+
if (remaining.length > 0) guide(remaining);
|
|
16918
|
+
return collected;
|
|
16919
|
+
}
|
|
16920
|
+
|
|
16681
16921
|
// src/git-info.ts
|
|
16682
16922
|
import { execFile } from "child_process";
|
|
16683
16923
|
var SUPPORTED_REMOTE = [
|
|
@@ -16857,7 +17097,7 @@ function slugify2(value) {
|
|
|
16857
17097
|
async function projectName(sourceDir) {
|
|
16858
17098
|
try {
|
|
16859
17099
|
const pkg = JSON.parse(
|
|
16860
|
-
await readFile7(
|
|
17100
|
+
await readFile7(join15(sourceDir, "package.json"), "utf8")
|
|
16861
17101
|
);
|
|
16862
17102
|
if (pkg.name) return pkg.name;
|
|
16863
17103
|
} catch {
|
|
@@ -16868,8 +17108,8 @@ var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.tu
|
|
|
16868
17108
|
function resolveBin(name) {
|
|
16869
17109
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
16870
17110
|
for (; ; ) {
|
|
16871
|
-
const candidate =
|
|
16872
|
-
if (
|
|
17111
|
+
const candidate = join15(dir, "node_modules", ".bin", name);
|
|
17112
|
+
if (existsSync6(candidate)) return candidate;
|
|
16873
17113
|
const parent = dirname(dir);
|
|
16874
17114
|
if (parent === dir) return name;
|
|
16875
17115
|
dir = parent;
|
|
@@ -16893,7 +17133,7 @@ async function detectPackageManagerLenient(root) {
|
|
|
16893
17133
|
}
|
|
16894
17134
|
function localDependencies(sourceDir) {
|
|
16895
17135
|
return {
|
|
16896
|
-
createWorkspace: () => mkdtemp2(
|
|
17136
|
+
createWorkspace: () => mkdtemp2(join15(tmpdir2(), "naro-build-")),
|
|
16897
17137
|
removeWorkspace: (path) => rm2(path, { recursive: true, force: true }),
|
|
16898
17138
|
async runCommand(spec, observer) {
|
|
16899
17139
|
if (spec.command === "git" && spec.args[0] === "clone") {
|
|
@@ -16955,6 +17195,32 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
16955
17195
|
else if (git.repositoryUrl) log2(`\u25B8 linking ${git.repositoryUrl}`);
|
|
16956
17196
|
if (options.env && Object.keys(options.env).length > 0)
|
|
16957
17197
|
log2(`\u25B8 syncing ${String(Object.keys(options.env).length)} env var(s)`);
|
|
17198
|
+
const scanned = scanEnvReferences(sourceDir).map(
|
|
17199
|
+
(reference) => reference.key
|
|
17200
|
+
);
|
|
17201
|
+
let missing = [];
|
|
17202
|
+
if (scanned.length > 0) {
|
|
17203
|
+
let existing = /* @__PURE__ */ new Set();
|
|
17204
|
+
if (options.projectId) {
|
|
17205
|
+
try {
|
|
17206
|
+
const rows = await client.env.list(options.projectId);
|
|
17207
|
+
existing = new Set(rows.map((row) => row.key));
|
|
17208
|
+
} catch {
|
|
17209
|
+
}
|
|
17210
|
+
}
|
|
17211
|
+
const provided = options.env ?? {};
|
|
17212
|
+
missing = scanned.filter((key) => !existing.has(key) && !(key in provided));
|
|
17213
|
+
}
|
|
17214
|
+
let collected = {};
|
|
17215
|
+
if (missing.length > 0) {
|
|
17216
|
+
log2(
|
|
17217
|
+
`\u25B8 ${String(missing.length)} env var(s) needed, not set on this project: ${missing.join(", ")}`
|
|
17218
|
+
);
|
|
17219
|
+
if (options.onMissingEnv) collected = await options.onMissingEnv(missing);
|
|
17220
|
+
else for (const line of envAddGuidance(missing)) log2(line);
|
|
17221
|
+
}
|
|
17222
|
+
const missingEnvKeys = missing.filter((key) => !(key in collected));
|
|
17223
|
+
const envToSend = { ...options.env, ...collected };
|
|
16958
17224
|
log2("\u25B8 starting deployment");
|
|
16959
17225
|
const ctx = await client.deployments.createCli({
|
|
16960
17226
|
...options.projectId ? { projectId: options.projectId } : { name },
|
|
@@ -16964,7 +17230,7 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
16964
17230
|
...options.rootDirectory != null && {
|
|
16965
17231
|
rootDirectory: options.rootDirectory
|
|
16966
17232
|
},
|
|
16967
|
-
...
|
|
17233
|
+
...Object.keys(envToSend).length > 0 && { env: envToSend }
|
|
16968
17234
|
});
|
|
16969
17235
|
if (ctx.envSkipped?.length)
|
|
16970
17236
|
log2(`\u26A0 skipped env: ${ctx.envSkipped.join(", ")}`);
|
|
@@ -16984,7 +17250,7 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
16984
17250
|
repositoryUrl: sourceDir,
|
|
16985
17251
|
branch: ctx.branch,
|
|
16986
17252
|
rootDirectory: options.rootDirectory ?? ctx.rootDirectory,
|
|
16987
|
-
env: { ...ctx.env, ...
|
|
17253
|
+
env: { ...ctx.env, ...envToSend }
|
|
16988
17254
|
},
|
|
16989
17255
|
observer
|
|
16990
17256
|
);
|
|
@@ -17017,7 +17283,8 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
17017
17283
|
projectId: ctx.projectId,
|
|
17018
17284
|
status: detail.deployment.status,
|
|
17019
17285
|
url: url2,
|
|
17020
|
-
production
|
|
17286
|
+
production,
|
|
17287
|
+
missingEnvKeys
|
|
17021
17288
|
};
|
|
17022
17289
|
}
|
|
17023
17290
|
async function waitForFinish(client, ctx, log2) {
|
|
@@ -17037,6 +17304,7 @@ function deploymentUrl(detail, ctx, production) {
|
|
|
17037
17304
|
}
|
|
17038
17305
|
|
|
17039
17306
|
export {
|
|
17307
|
+
parseGitHubRemote,
|
|
17040
17308
|
NaroError,
|
|
17041
17309
|
isNaroError,
|
|
17042
17310
|
toNaroError,
|
|
@@ -17052,14 +17320,15 @@ export {
|
|
|
17052
17320
|
openBrowser,
|
|
17053
17321
|
isTerminalStatus,
|
|
17054
17322
|
NaroClient,
|
|
17055
|
-
readProjectEnv,
|
|
17056
17323
|
serializeDotenv,
|
|
17057
17324
|
readProjectLink,
|
|
17058
17325
|
writeProjectLink,
|
|
17059
17326
|
removeProjectLink,
|
|
17060
17327
|
ensureGitignoreHasNaro,
|
|
17328
|
+
promptForMissingEnv,
|
|
17329
|
+
detectGitInfo,
|
|
17061
17330
|
parseDotenv2 as parseDotenv,
|
|
17062
|
-
readProjectEnv2,
|
|
17331
|
+
readProjectEnv2 as readProjectEnv,
|
|
17063
17332
|
eventsObserver,
|
|
17064
17333
|
slugify2 as slugify,
|
|
17065
17334
|
deployDirectory
|
package/dist/deploy.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface DeployOptions {
|
|
|
35
35
|
fetch?: typeof fetch;
|
|
36
36
|
/** Test seam: replaces the local OpenNext build. */
|
|
37
37
|
executor?: DeployBuildExecutor;
|
|
38
|
+
/**
|
|
39
|
+
* 소스가 참조하지만 프로젝트에 없는 env 키를 받아, 등록할 값을 돌려준다.
|
|
40
|
+
* 대화형 CLI만 전달한다 — MCP·CI는 생략하고 안내만 받는다.
|
|
41
|
+
*/
|
|
42
|
+
onMissingEnv?: (keys: string[]) => Promise<Record<string, string>>;
|
|
38
43
|
}
|
|
39
44
|
export interface DeployResult {
|
|
40
45
|
deploymentId: string;
|
|
@@ -42,6 +47,8 @@ export interface DeployResult {
|
|
|
42
47
|
status: DeploymentStatus;
|
|
43
48
|
url: string;
|
|
44
49
|
production: boolean;
|
|
50
|
+
/** 끝까지 채워지지 않은 키. */
|
|
51
|
+
missingEnvKeys: string[];
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
54
|
* The full CLI deploy: authenticate, register the deployment with the
|
package/dist/deploy.js
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Output } from "./output";
|
|
2
|
+
import type { Prompter } from "./prompt";
|
|
3
|
+
export interface EnvConsentContext {
|
|
4
|
+
cwd: string;
|
|
5
|
+
prompter: Prompter;
|
|
6
|
+
output: Output;
|
|
7
|
+
/** 테스트 주입용. 기본값은 SDK의 readProjectEnv. */
|
|
8
|
+
readEnv?: (dir: string) => Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export declare function maskEnvValue(key: string, value: string): string;
|
|
11
|
+
/** 동의를 구하기 전에는 존재만 확인한다. 내용은 열지 않는다. */
|
|
12
|
+
export declare function findDotenvFile(dir: string): string | null;
|
|
13
|
+
export declare function envAddGuidance(keys: string[]): string[];
|
|
14
|
+
/**
|
|
15
|
+
* 누락된 env 키를 채운다. dotenv 파일은 사용자가 명시적으로 동의한 뒤에만
|
|
16
|
+
* 열리고, 그때도 요청된 키의 값만 꺼낸다 — 파일의 나머지 내용은 전송되지 않는다.
|
|
17
|
+
*/
|
|
18
|
+
export declare function promptForMissingEnv(ctx: EnvConsentContext, keys: string[]): Promise<Record<string, string>>;
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
controlPlaneUrl,
|
|
7
7
|
createAuthClient,
|
|
8
8
|
deployDirectory,
|
|
9
|
+
detectGitInfo,
|
|
9
10
|
ensureGitignoreHasNaro,
|
|
10
11
|
isExpired,
|
|
11
12
|
isNaroError,
|
|
@@ -13,7 +14,8 @@ import {
|
|
|
13
14
|
loadStoredSession,
|
|
14
15
|
login,
|
|
15
16
|
openBrowser,
|
|
16
|
-
|
|
17
|
+
parseGitHubRemote,
|
|
18
|
+
promptForMissingEnv,
|
|
17
19
|
readProjectLink,
|
|
18
20
|
refreshStoredSession,
|
|
19
21
|
removeProjectLink,
|
|
@@ -23,7 +25,7 @@ import {
|
|
|
23
25
|
slugify,
|
|
24
26
|
toNaroError,
|
|
25
27
|
writeProjectLink
|
|
26
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-YLFLDW2H.js";
|
|
27
29
|
|
|
28
30
|
// src/args.ts
|
|
29
31
|
var BOOLEAN_FLAGS = {
|
|
@@ -154,7 +156,7 @@ var Output = class {
|
|
|
154
156
|
success: false,
|
|
155
157
|
code: naroError.code,
|
|
156
158
|
message: naroError.message,
|
|
157
|
-
...naroError.details
|
|
159
|
+
...naroError.details
|
|
158
160
|
},
|
|
159
161
|
null,
|
|
160
162
|
2
|
|
@@ -411,8 +413,162 @@ async function runToken(context) {
|
|
|
411
413
|
);
|
|
412
414
|
}
|
|
413
415
|
|
|
416
|
+
// src/commands/git.ts
|
|
417
|
+
var defaults = {
|
|
418
|
+
detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
|
|
419
|
+
openBrowser,
|
|
420
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
421
|
+
};
|
|
422
|
+
function connected(context, project) {
|
|
423
|
+
const branch = project.defaultBranch;
|
|
424
|
+
context.output.result(
|
|
425
|
+
{
|
|
426
|
+
projectId: project.id,
|
|
427
|
+
repositoryFullName: project.repositoryFullName,
|
|
428
|
+
autoDeploy: true
|
|
429
|
+
},
|
|
430
|
+
[
|
|
431
|
+
`\u2705 Connected to ${project.repositoryFullName ?? "the repository"}.`,
|
|
432
|
+
` Pushes to ${branch} now deploy automatically.`,
|
|
433
|
+
"",
|
|
434
|
+
" To turn this off: naro git disconnect"
|
|
435
|
+
]
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
async function runGitConnect(context, overrides = {}) {
|
|
439
|
+
const deps = { ...defaults, ...overrides };
|
|
440
|
+
const { output } = context;
|
|
441
|
+
const explicit = context.positionals[0];
|
|
442
|
+
let fullName;
|
|
443
|
+
if (explicit) {
|
|
444
|
+
const parsed = parseGitHubRemote(explicit) ?? parseGitHubRemote(`https://github.com/${explicit}`);
|
|
445
|
+
if (!parsed)
|
|
446
|
+
throw new NaroError(
|
|
447
|
+
"VALIDATION_FAILED",
|
|
448
|
+
`"${explicit}" is not an owner/repo pair.`
|
|
449
|
+
);
|
|
450
|
+
fullName = parsed.fullName;
|
|
451
|
+
} else {
|
|
452
|
+
const remote = await deps.detectRemote(context.cwd);
|
|
453
|
+
const parsed = remote ? parseGitHubRemote(remote) : null;
|
|
454
|
+
if (!parsed) {
|
|
455
|
+
throw new NaroError(
|
|
456
|
+
"NO_GIT_REMOTE",
|
|
457
|
+
remote ? `Could not read a GitHub owner/repo from "${remote}". Pass it directly: naro git connect <owner/repo>` : "This directory has no GitHub origin remote. Pass one directly: naro git connect <owner/repo>"
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
fullName = parsed.fullName;
|
|
461
|
+
}
|
|
462
|
+
const client = await context.client();
|
|
463
|
+
const projectId = await context.resolveProjectId();
|
|
464
|
+
const first = await client.projects.connectGit(projectId, fullName);
|
|
465
|
+
if (first.ok) {
|
|
466
|
+
connected(context, first.project);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (!first.installUrl) {
|
|
470
|
+
throw new NaroError(
|
|
471
|
+
"REPOSITORY_NOT_ACCESSIBLE",
|
|
472
|
+
`Naro cannot reach ${fullName}, and no install URL was offered. Connect it from the dashboard.`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
output.log(`
|
|
476
|
+
\u25B8 Naro needs access to ${fullName}.`);
|
|
477
|
+
output.log(` Opening ${first.installUrl}`);
|
|
478
|
+
deps.openBrowser(first.installUrl);
|
|
479
|
+
output.log(" Waiting for the installation to finish...\n");
|
|
480
|
+
await waitForInstall(context, projectId, fullName, deps);
|
|
481
|
+
}
|
|
482
|
+
async function waitForInstall(context, projectId, fullName, deps) {
|
|
483
|
+
const client = await context.client();
|
|
484
|
+
const project = await client.projects.get(projectId);
|
|
485
|
+
const teams = await client.teams.list();
|
|
486
|
+
const teamSlug = teams.find((team) => team.id === project.teamId)?.slug;
|
|
487
|
+
if (teamSlug) {
|
|
488
|
+
const attempts = deps.pollAttempts ?? 60;
|
|
489
|
+
const interval = deps.pollIntervalMs ?? 3e3;
|
|
490
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
491
|
+
await deps.sleep(interval);
|
|
492
|
+
let reachable = false;
|
|
493
|
+
try {
|
|
494
|
+
const repositories = await client.github.listRepositories(teamSlug);
|
|
495
|
+
reachable = repositories.some(
|
|
496
|
+
(repository) => repository.fullName.toLowerCase() === fullName.toLowerCase()
|
|
497
|
+
);
|
|
498
|
+
} catch {
|
|
499
|
+
}
|
|
500
|
+
if (!reachable) continue;
|
|
501
|
+
const retry = await client.projects.connectGit(projectId, fullName);
|
|
502
|
+
if (retry.ok) {
|
|
503
|
+
connected(context, retry.project);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
context.output.result(
|
|
509
|
+
{ projectId, repositoryFullName: null, autoDeploy: false },
|
|
510
|
+
[
|
|
511
|
+
`\u25B8 Still waiting on access to ${fullName}.`,
|
|
512
|
+
" Finish the installation, then run: naro git connect"
|
|
513
|
+
]
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
async function runGitDisconnect(context) {
|
|
517
|
+
const client = await context.client();
|
|
518
|
+
const projectId = await context.resolveProjectId();
|
|
519
|
+
const project = await client.projects.disconnectGit(projectId);
|
|
520
|
+
context.output.result({ projectId: project.id, autoDeploy: false }, [
|
|
521
|
+
`\u2705 ${project.name} no longer deploys on push.`,
|
|
522
|
+
" Deploy from here with: naro deploy"
|
|
523
|
+
]);
|
|
524
|
+
}
|
|
525
|
+
async function offerAutoDeploy(context, projectId, overrides = {}) {
|
|
526
|
+
const deps = { ...defaults, ...overrides };
|
|
527
|
+
if (overrides.canPrompt === false) return;
|
|
528
|
+
try {
|
|
529
|
+
if (readProjectLink(context.cwd)?.autoDeployDeclined === true) return;
|
|
530
|
+
const remote = await deps.detectRemote(context.cwd);
|
|
531
|
+
const parsed = remote ? parseGitHubRemote(remote) : null;
|
|
532
|
+
if (!parsed) return;
|
|
533
|
+
const client = await context.client();
|
|
534
|
+
const project = await client.projects.get(projectId);
|
|
535
|
+
if (project.repositoryFullName) return;
|
|
536
|
+
const wanted = await context.prompter.confirm(
|
|
537
|
+
"Should your changes deploy automatically from now on?"
|
|
538
|
+
);
|
|
539
|
+
if (!wanted) {
|
|
540
|
+
const link = readProjectLink(context.cwd);
|
|
541
|
+
writeProjectLink(context.cwd, {
|
|
542
|
+
...link ?? { projectId },
|
|
543
|
+
autoDeployDeclined: true
|
|
544
|
+
});
|
|
545
|
+
context.output.log(
|
|
546
|
+
"\u25B8 Not now. Later, if you change your mind: naro git connect"
|
|
547
|
+
);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const result = await client.projects.connectGit(projectId, parsed.fullName);
|
|
551
|
+
if (result.ok) {
|
|
552
|
+
connected(context, result.project);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (!result.installUrl) return;
|
|
556
|
+
context.output.log(`
|
|
557
|
+
\u25B8 Naro needs access to ${parsed.fullName}.`);
|
|
558
|
+
context.output.log(` Opening ${result.installUrl}`);
|
|
559
|
+
deps.openBrowser(result.installUrl);
|
|
560
|
+
context.output.log(" Waiting for the installation to finish...\n");
|
|
561
|
+
await waitForInstall(context, projectId, parsed.fullName, deps);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
context.output.log(
|
|
564
|
+
`\u25B8 Could not set up automatic deploys (${error instanceof Error ? error.message : String(error)}).`
|
|
565
|
+
);
|
|
566
|
+
context.output.log(" Try later with: naro git connect");
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
414
570
|
// src/commands/deploy.ts
|
|
415
|
-
async function runDeploy(context) {
|
|
571
|
+
async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDeploy }) {
|
|
416
572
|
const { flags, output } = context;
|
|
417
573
|
const link = flags.project ? { projectId: flags.project } : readProjectLink(context.cwd);
|
|
418
574
|
if (flags.prod && !flags.yes && !flags.json && isInteractive()) {
|
|
@@ -422,12 +578,8 @@ async function runDeploy(context) {
|
|
|
422
578
|
output.log(`
|
|
423
579
|
\u{1F680} Deploying ${context.cwd}
|
|
424
580
|
`);
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
output.log(
|
|
428
|
-
"\u25B8 using project env vars (manage them with `naro env`, sync local files with `naro env pull`)"
|
|
429
|
-
);
|
|
430
|
-
const result = await deployDirectory(
|
|
581
|
+
const canPrompt = !flags.json && !flags.yes && isInteractive();
|
|
582
|
+
const result = await deps.deploy(
|
|
431
583
|
context.cwd,
|
|
432
584
|
{
|
|
433
585
|
name: flags.name,
|
|
@@ -435,10 +587,12 @@ async function runDeploy(context) {
|
|
|
435
587
|
rootDirectory: flags.root ?? null,
|
|
436
588
|
branch: flags.branch,
|
|
437
589
|
production: flags.prod,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
590
|
+
...canPrompt && {
|
|
591
|
+
onMissingEnv: (keys) => promptForMissingEnv(
|
|
592
|
+
{ cwd: context.cwd, prompter: context.prompter, output },
|
|
593
|
+
keys
|
|
594
|
+
)
|
|
595
|
+
},
|
|
442
596
|
token: flags.token,
|
|
443
597
|
allowLogin: !flags.json && !flags.yes
|
|
444
598
|
},
|
|
@@ -450,13 +604,15 @@ async function runDeploy(context) {
|
|
|
450
604
|
output.log("\u25B8 added .naro to .gitignore");
|
|
451
605
|
output.log("\u25B8 linked (.naro/project.json)");
|
|
452
606
|
}
|
|
607
|
+
await deps.offerAutoDeploy?.(context, result.projectId, { canPrompt });
|
|
453
608
|
context.output.result(
|
|
454
609
|
{
|
|
455
610
|
deploymentId: result.deploymentId,
|
|
456
611
|
projectId: result.projectId,
|
|
457
612
|
status: result.status,
|
|
458
613
|
url: result.url,
|
|
459
|
-
target: result.production ? "production" : "preview"
|
|
614
|
+
target: result.production ? "production" : "preview",
|
|
615
|
+
missingEnvKeys: result.missingEnvKeys
|
|
460
616
|
},
|
|
461
617
|
[
|
|
462
618
|
"",
|
|
@@ -1092,6 +1248,9 @@ Usage:
|
|
|
1092
1248
|
naro deployments List deployments of the linked project
|
|
1093
1249
|
naro inspect <deployment-id> Show one deployment + build logs
|
|
1094
1250
|
|
|
1251
|
+
naro git connect [owner/repo] Deploy on every push (defaults to your origin remote)
|
|
1252
|
+
naro git disconnect Stop deploying on push
|
|
1253
|
+
|
|
1095
1254
|
naro logs [deployment-id] Runtime logs (or build logs of a deployment)
|
|
1096
1255
|
[--type build|runtime] [--env production|preview] [--level error] [--follow]
|
|
1097
1256
|
|
|
@@ -1190,6 +1349,15 @@ function route(parsed) {
|
|
|
1190
1349
|
return { handler: runPromote, positionals };
|
|
1191
1350
|
case "rollback":
|
|
1192
1351
|
return { handler: runRollback, positionals };
|
|
1352
|
+
case "git":
|
|
1353
|
+
switch (positionals[0]) {
|
|
1354
|
+
case "connect":
|
|
1355
|
+
return { handler: runGitConnect, positionals: positionals.slice(1) };
|
|
1356
|
+
case "disconnect":
|
|
1357
|
+
return { handler: runGitDisconnect, positionals: [] };
|
|
1358
|
+
default:
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1193
1361
|
default:
|
|
1194
1362
|
return null;
|
|
1195
1363
|
}
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface OutputStreams {
|
|
2
|
+
stdout(text: string): void;
|
|
3
|
+
stderr(text: string): void;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* One Output per invocation. Human mode: progress + results on stdout.
|
|
7
|
+
* JSON mode (--json): stdout carries exactly one machine-readable JSON
|
|
8
|
+
* object; every human-facing line moves to stderr so agents never have to
|
|
9
|
+
* parse log noise.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Output {
|
|
12
|
+
readonly json: boolean;
|
|
13
|
+
readonly debug: boolean;
|
|
14
|
+
private readonly streams;
|
|
15
|
+
private resultPrinted;
|
|
16
|
+
constructor(json: boolean, debug: boolean, streams?: OutputStreams);
|
|
17
|
+
/** Progress/log line for humans. */
|
|
18
|
+
log(message: string): void;
|
|
19
|
+
/** Extra diagnostics, only with --debug. */
|
|
20
|
+
debugLog(message: string): void;
|
|
21
|
+
/**
|
|
22
|
+
* Final command result. JSON mode prints {success:true, ...data}; human
|
|
23
|
+
* mode prints the lines instead.
|
|
24
|
+
*/
|
|
25
|
+
result(data: Record<string, unknown>, humanLines?: string[]): void;
|
|
26
|
+
/** Command failure: stable {success:false, code, message} in JSON mode. */
|
|
27
|
+
fail(error: unknown): void;
|
|
28
|
+
get hasResult(): boolean;
|
|
29
|
+
}
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Prompter {
|
|
2
|
+
/** Free-text question. */
|
|
3
|
+
ask(question: string): Promise<string>;
|
|
4
|
+
/** Question with hidden echo (secrets). */
|
|
5
|
+
askHidden(question: string): Promise<string>;
|
|
6
|
+
/** y/N confirmation, defaults to No. */
|
|
7
|
+
confirm(question: string): Promise<boolean>;
|
|
8
|
+
/** Pick one option by number; returns its index. */
|
|
9
|
+
select(question: string, options: string[]): Promise<number>;
|
|
10
|
+
}
|
|
11
|
+
export declare function isInteractive(): boolean;
|
|
12
|
+
/** Reads all of stdin — the CI path for piped secret values. */
|
|
13
|
+
export declare function readStdin(): Promise<string>;
|
|
14
|
+
export declare function createPrompter(options: {
|
|
15
|
+
json: boolean;
|
|
16
|
+
yes: boolean;
|
|
17
|
+
}): Prompter;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"naro": "./dist/index.js"
|
|
@@ -29,15 +29,14 @@
|
|
|
29
29
|
"typescript": "7.0.2",
|
|
30
30
|
"vitest": "3.2.7",
|
|
31
31
|
"@next-deploy/build": "0.1.0",
|
|
32
|
-
"@next-deploy/config": "0.1.0",
|
|
33
32
|
"@next-deploy/core": "0.1.0",
|
|
33
|
+
"@next-deploy/config": "0.1.0",
|
|
34
34
|
"@next-deploy/sdk": "0.1.0"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsup && pnpm build:types",
|
|
38
38
|
"build:types": "tsc --project tsconfig.build.json",
|
|
39
39
|
"test": "vitest run",
|
|
40
|
-
"typecheck": "tsc --noEmit"
|
|
41
|
-
"lint": "tsc --noEmit"
|
|
40
|
+
"typecheck": "tsc --noEmit"
|
|
42
41
|
}
|
|
43
42
|
}
|