naro-cli 0.2.1 → 0.4.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 +120 -4
- package/dist/artifact-types.d.ts +3 -0
- package/dist/{chunk-IC4R5EYE.js → chunk-YLFLDW2H.js} +1602 -399
- package/dist/deploy.d.ts +46 -26
- package/dist/deploy.js +3 -9
- package/dist/env-consent.d.ts +18 -0
- package/dist/index.js +1401 -65
- package/dist/output.d.ts +29 -0
- package/dist/prompt.d.ts +17 -0
- package/package.json +13 -12
- package/dist/callback-server.d.ts +0 -10
- package/dist/index.d.ts +0 -7
- package/dist/login.d.ts +0 -10
- package/dist/session.d.ts +0 -16
- package/dist/supabase-auth.d.ts +0 -11
package/dist/index.js
CHANGED
|
@@ -1,91 +1,1427 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
NaroClient,
|
|
4
|
+
NaroError,
|
|
5
|
+
clearSession,
|
|
6
|
+
controlPlaneUrl,
|
|
7
|
+
createAuthClient,
|
|
3
8
|
deployDirectory,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
detectGitInfo,
|
|
10
|
+
ensureGitignoreHasNaro,
|
|
11
|
+
isExpired,
|
|
12
|
+
isNaroError,
|
|
13
|
+
isTerminalStatus,
|
|
14
|
+
loadStoredSession,
|
|
15
|
+
login,
|
|
16
|
+
openBrowser,
|
|
17
|
+
parseGitHubRemote,
|
|
18
|
+
promptForMissingEnv,
|
|
19
|
+
readProjectLink,
|
|
20
|
+
refreshStoredSession,
|
|
21
|
+
removeProjectLink,
|
|
22
|
+
resolveAuth,
|
|
23
|
+
saveSession,
|
|
24
|
+
serializeDotenv,
|
|
25
|
+
slugify,
|
|
26
|
+
toNaroError,
|
|
27
|
+
writeProjectLink
|
|
28
|
+
} from "./chunk-YLFLDW2H.js";
|
|
9
29
|
|
|
10
|
-
// src/
|
|
11
|
-
|
|
12
|
-
|
|
30
|
+
// src/args.ts
|
|
31
|
+
var BOOLEAN_FLAGS = {
|
|
32
|
+
"--json": "json",
|
|
33
|
+
"--yes": "yes",
|
|
34
|
+
"-y": "yes",
|
|
35
|
+
"--debug": "debug",
|
|
36
|
+
"--help": "help",
|
|
37
|
+
"-h": "help",
|
|
38
|
+
"--version": "version",
|
|
39
|
+
"-v": "version",
|
|
40
|
+
"--prod": "prod",
|
|
41
|
+
"--production": "prod",
|
|
42
|
+
"--follow": "follow",
|
|
43
|
+
"-f": "follow",
|
|
44
|
+
"--reveal": "reveal"
|
|
45
|
+
};
|
|
46
|
+
var STRING_FLAGS = {
|
|
47
|
+
"--token": "token",
|
|
48
|
+
"--project": "project",
|
|
49
|
+
"--team": "team",
|
|
50
|
+
"--cwd": "cwd",
|
|
51
|
+
"--name": "name",
|
|
52
|
+
"--root": "root",
|
|
53
|
+
"--branch": "branch",
|
|
54
|
+
"--type": "type",
|
|
55
|
+
"--env": "env",
|
|
56
|
+
"--level": "level"
|
|
57
|
+
};
|
|
58
|
+
function parseArgs(argv) {
|
|
59
|
+
const flags = {
|
|
60
|
+
json: false,
|
|
61
|
+
yes: false,
|
|
62
|
+
debug: false,
|
|
63
|
+
help: false,
|
|
64
|
+
version: false,
|
|
65
|
+
prod: false,
|
|
66
|
+
follow: false,
|
|
67
|
+
reveal: false
|
|
68
|
+
};
|
|
69
|
+
const positionals = [];
|
|
70
|
+
const unknown = [];
|
|
13
71
|
for (let i = 0; i < argv.length; i++) {
|
|
14
72
|
const arg = argv[i];
|
|
15
|
-
if (arg ===
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
73
|
+
if (arg === void 0) continue;
|
|
74
|
+
const booleanKey = BOOLEAN_FLAGS[arg];
|
|
75
|
+
if (booleanKey) {
|
|
76
|
+
flags[booleanKey] = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const eq = arg.indexOf("=");
|
|
80
|
+
const stringKey = STRING_FLAGS[arg] ?? (eq > 0 ? STRING_FLAGS[arg.slice(0, eq)] : void 0);
|
|
81
|
+
if (stringKey) {
|
|
82
|
+
if (eq > 0 && STRING_FLAGS[arg.slice(0, eq)]) {
|
|
83
|
+
flags[stringKey] = arg.slice(
|
|
84
|
+
eq + 1
|
|
85
|
+
);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const value = argv[i + 1];
|
|
89
|
+
if (value === void 0 || value.startsWith("-") && value !== "-") {
|
|
90
|
+
unknown.push(`${arg} (missing value)`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
i += 1;
|
|
94
|
+
flags[stringKey] = value;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (arg.startsWith("-") && arg !== "-") {
|
|
98
|
+
unknown.push(arg);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
positionals.push(arg);
|
|
19
102
|
}
|
|
20
|
-
|
|
103
|
+
const [command, ...rest] = positionals;
|
|
104
|
+
return { command, positionals: rest, flags, unknown };
|
|
21
105
|
}
|
|
22
|
-
var HELP = `naro \u2014 deploy the current app to naro
|
|
23
106
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
--prod Publish to production ({name}.naro.sh) instead of preview
|
|
35
|
-
|
|
36
|
-
Credentials:
|
|
37
|
-
Run \`naro login\` to authenticate via GitHub.
|
|
38
|
-
\`naro deploy\` signs you in automatically (opens your browser) when needed.
|
|
39
|
-
Set NARO_CONTROL_PLANE_URL to use a custom control plane (defaults to https://naro.sh).
|
|
40
|
-
`;
|
|
41
|
-
async function main() {
|
|
42
|
-
const [command, ...rest] = process.argv.slice(2);
|
|
43
|
-
if (!command || command === "help" || command === "--help") {
|
|
44
|
-
process.stdout.write(HELP);
|
|
45
|
-
return;
|
|
107
|
+
// src/output.ts
|
|
108
|
+
var processStreams = {
|
|
109
|
+
stdout: (text) => process.stdout.write(text),
|
|
110
|
+
stderr: (text) => process.stderr.write(text)
|
|
111
|
+
};
|
|
112
|
+
var Output = class {
|
|
113
|
+
constructor(json, debug, streams = processStreams) {
|
|
114
|
+
this.json = json;
|
|
115
|
+
this.debug = debug;
|
|
116
|
+
this.streams = streams;
|
|
46
117
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
118
|
+
resultPrinted = false;
|
|
119
|
+
/** Progress/log line for humans. */
|
|
120
|
+
log(message) {
|
|
121
|
+
if (this.json) this.streams.stderr(`${message}
|
|
122
|
+
`);
|
|
123
|
+
else this.streams.stdout(`${message}
|
|
124
|
+
`);
|
|
125
|
+
}
|
|
126
|
+
/** Extra diagnostics, only with --debug. */
|
|
127
|
+
debugLog(message) {
|
|
128
|
+
if (this.debug) this.streams.stderr(`[debug] ${message}
|
|
129
|
+
`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Final command result. JSON mode prints {success:true, ...data}; human
|
|
133
|
+
* mode prints the lines instead.
|
|
134
|
+
*/
|
|
135
|
+
result(data, humanLines = []) {
|
|
136
|
+
this.resultPrinted = true;
|
|
137
|
+
if (this.json) {
|
|
138
|
+
this.streams.stdout(
|
|
139
|
+
`${JSON.stringify({ success: true, ...data }, null, 2)}
|
|
140
|
+
`
|
|
141
|
+
);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for (const line of humanLines) this.streams.stdout(`${line}
|
|
145
|
+
`);
|
|
146
|
+
}
|
|
147
|
+
/** Command failure: stable {success:false, code, message} in JSON mode. */
|
|
148
|
+
fail(error) {
|
|
149
|
+
const naroError = isNaroError(error) ? error : toNaroError(error);
|
|
150
|
+
this.resultPrinted = true;
|
|
54
151
|
process.exitCode = 1;
|
|
55
|
-
|
|
152
|
+
if (this.json) {
|
|
153
|
+
this.streams.stdout(
|
|
154
|
+
`${JSON.stringify(
|
|
155
|
+
{
|
|
156
|
+
success: false,
|
|
157
|
+
code: naroError.code,
|
|
158
|
+
message: naroError.message,
|
|
159
|
+
...naroError.details
|
|
160
|
+
},
|
|
161
|
+
null,
|
|
162
|
+
2
|
|
163
|
+
)}
|
|
164
|
+
`
|
|
165
|
+
);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.streams.stderr(`\u2717 ${naroError.message}
|
|
169
|
+
`);
|
|
170
|
+
if (this.debug && naroError.cause instanceof Error) {
|
|
171
|
+
this.streams.stderr(`[debug] cause: ${naroError.cause.stack ?? ""}
|
|
172
|
+
`);
|
|
173
|
+
}
|
|
56
174
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
\u{1F680} Deploying ${cwd}
|
|
175
|
+
get hasResult() {
|
|
176
|
+
return this.resultPrinted;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
62
179
|
|
|
180
|
+
// src/prompt.ts
|
|
181
|
+
import { createInterface } from "readline";
|
|
182
|
+
import { Writable } from "stream";
|
|
183
|
+
function isInteractive() {
|
|
184
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
185
|
+
}
|
|
186
|
+
async function readStdin() {
|
|
187
|
+
let data = "";
|
|
188
|
+
for await (const chunk of process.stdin) data += String(chunk);
|
|
189
|
+
return data.replace(/\r?\n$/, "");
|
|
190
|
+
}
|
|
191
|
+
var TtyPrompter = class {
|
|
192
|
+
async ask(question) {
|
|
193
|
+
const rl = createInterface({
|
|
194
|
+
input: process.stdin,
|
|
195
|
+
output: process.stdout
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
return await new Promise(
|
|
199
|
+
(resolve) => rl.question(question, resolve)
|
|
200
|
+
);
|
|
201
|
+
} finally {
|
|
202
|
+
rl.close();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async askHidden(question) {
|
|
206
|
+
let muted = false;
|
|
207
|
+
const output = new Writable({
|
|
208
|
+
write(chunk, _enc, done) {
|
|
209
|
+
if (!muted) process.stdout.write(chunk);
|
|
210
|
+
done();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
const rl = createInterface({
|
|
214
|
+
input: process.stdin,
|
|
215
|
+
output,
|
|
216
|
+
terminal: true
|
|
217
|
+
});
|
|
218
|
+
try {
|
|
219
|
+
const answer = new Promise(
|
|
220
|
+
(resolve) => rl.question(question, resolve)
|
|
221
|
+
);
|
|
222
|
+
muted = true;
|
|
223
|
+
const value = await answer;
|
|
224
|
+
process.stdout.write("\n");
|
|
225
|
+
return value;
|
|
226
|
+
} finally {
|
|
227
|
+
rl.close();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async confirm(question) {
|
|
231
|
+
const answer = (await this.ask(`${question} (y/N) `)).trim().toLowerCase();
|
|
232
|
+
return answer === "y" || answer === "yes";
|
|
233
|
+
}
|
|
234
|
+
async select(question, options) {
|
|
235
|
+
process.stdout.write(`${question}
|
|
236
|
+
`);
|
|
237
|
+
options.forEach((option, index) => {
|
|
238
|
+
process.stdout.write(` ${String(index + 1)}. ${option}
|
|
63
239
|
`);
|
|
64
|
-
|
|
240
|
+
});
|
|
241
|
+
for (; ; ) {
|
|
242
|
+
const answer = Number.parseInt(
|
|
243
|
+
(await this.ask(`Choose 1-${String(options.length)}: `)).trim(),
|
|
244
|
+
10
|
|
245
|
+
);
|
|
246
|
+
if (Number.isInteger(answer) && answer >= 1 && answer <= options.length)
|
|
247
|
+
return answer - 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
var NonInteractivePrompter = class {
|
|
252
|
+
constructor(assumeYes) {
|
|
253
|
+
this.assumeYes = assumeYes;
|
|
254
|
+
}
|
|
255
|
+
refuse(question) {
|
|
256
|
+
throw new NaroError(
|
|
257
|
+
"VALIDATION_FAILED",
|
|
258
|
+
`Interactive input required (${question.trim()}). Pass the value via flags/stdin or run without --json in a terminal.`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
async ask(question) {
|
|
262
|
+
this.refuse(question);
|
|
263
|
+
}
|
|
264
|
+
async askHidden(question) {
|
|
265
|
+
this.refuse(question);
|
|
266
|
+
}
|
|
267
|
+
async confirm() {
|
|
268
|
+
return this.assumeYes;
|
|
269
|
+
}
|
|
270
|
+
async select(question) {
|
|
271
|
+
this.refuse(question);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
function createPrompter(options) {
|
|
275
|
+
if (options.yes || options.json || !isInteractive())
|
|
276
|
+
return new NonInteractivePrompter(options.yes);
|
|
277
|
+
return new TtyPrompter();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/context.ts
|
|
281
|
+
function createContext(flags, positionals = []) {
|
|
282
|
+
const cwd = flags.cwd ?? process.cwd();
|
|
283
|
+
const output = new Output(flags.json, flags.debug);
|
|
284
|
+
const prompter = createPrompter({ json: flags.json, yes: flags.yes });
|
|
285
|
+
let resolvedAuth = null;
|
|
286
|
+
let resolvedClient = null;
|
|
287
|
+
const auth = async () => {
|
|
288
|
+
if (resolvedAuth) return resolvedAuth;
|
|
289
|
+
const interactive = !flags.json && !flags.yes && isInteractive() ? { openBrowser, log: (message) => output.log(message) } : false;
|
|
290
|
+
resolvedAuth = await resolveAuth({
|
|
291
|
+
explicitToken: flags.token,
|
|
292
|
+
interactiveLogin: interactive
|
|
293
|
+
});
|
|
294
|
+
return resolvedAuth;
|
|
295
|
+
};
|
|
296
|
+
return {
|
|
65
297
|
cwd,
|
|
298
|
+
flags,
|
|
299
|
+
positionals,
|
|
300
|
+
output,
|
|
301
|
+
prompter,
|
|
302
|
+
positionalOrThrow: (count, usage) => {
|
|
303
|
+
if (positionals.length < count)
|
|
304
|
+
throw new NaroError("VALIDATION_FAILED", usage);
|
|
305
|
+
return positionals.slice(0, count);
|
|
306
|
+
},
|
|
307
|
+
auth,
|
|
308
|
+
client: async () => {
|
|
309
|
+
if (resolvedClient) return resolvedClient;
|
|
310
|
+
const resolved = await auth();
|
|
311
|
+
resolvedClient = new NaroClient({
|
|
312
|
+
baseUrl: controlPlaneUrl(),
|
|
313
|
+
getAccessToken: () => resolved.getAccessToken()
|
|
314
|
+
});
|
|
315
|
+
return resolvedClient;
|
|
316
|
+
},
|
|
317
|
+
projectLink: () => readProjectLink(cwd),
|
|
318
|
+
resolveProjectId: async () => {
|
|
319
|
+
if (flags.project) return flags.project;
|
|
320
|
+
const link = readProjectLink(cwd);
|
|
321
|
+
if (link) return link.projectId;
|
|
322
|
+
throw new NaroError(
|
|
323
|
+
"PROJECT_NOT_LINKED",
|
|
324
|
+
"No project is linked here. Run `naro link` first, or pass --project <id>."
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/router.ts
|
|
331
|
+
import { readFileSync } from "fs";
|
|
332
|
+
import { dirname, join as join2 } from "path";
|
|
333
|
+
import { fileURLToPath } from "url";
|
|
334
|
+
|
|
335
|
+
// src/commands/auth.ts
|
|
336
|
+
async function runLogin(context) {
|
|
337
|
+
const { output } = context;
|
|
338
|
+
output.log("Opening your browser to sign in with GitHub...");
|
|
339
|
+
const session = await login({
|
|
340
|
+
openBrowser: (url) => {
|
|
341
|
+
output.log(`If your browser did not open, visit:
|
|
342
|
+
${url}`);
|
|
343
|
+
openBrowser(url);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
saveSession(session);
|
|
347
|
+
output.result(
|
|
348
|
+
{
|
|
349
|
+
user: {
|
|
350
|
+
id: session.user.id,
|
|
351
|
+
email: session.user.email,
|
|
352
|
+
github: session.user.github
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
[`\u2705 Logged in as ${session.user.email ?? session.user.id}`]
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
async function runLogout(context) {
|
|
359
|
+
const session = loadStoredSession();
|
|
360
|
+
if (session) {
|
|
361
|
+
try {
|
|
362
|
+
const client = createAuthClient();
|
|
363
|
+
await client.auth.setSession({
|
|
364
|
+
access_token: session.access_token,
|
|
365
|
+
refresh_token: session.refresh_token
|
|
366
|
+
});
|
|
367
|
+
await client.auth.signOut();
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
clearSession();
|
|
372
|
+
context.output.result({ loggedOut: true }, ["Logged out."]);
|
|
373
|
+
}
|
|
374
|
+
async function currentSession() {
|
|
375
|
+
let session = loadStoredSession();
|
|
376
|
+
if (!session)
|
|
377
|
+
throw new NaroError("NOT_LOGGED_IN", "Not logged in. Run `naro login`.");
|
|
378
|
+
if (isExpired(session)) {
|
|
379
|
+
try {
|
|
380
|
+
session = await refreshStoredSession(session.refresh_token);
|
|
381
|
+
saveSession(session);
|
|
382
|
+
} catch {
|
|
383
|
+
throw new NaroError(
|
|
384
|
+
"NOT_LOGGED_IN",
|
|
385
|
+
"Session expired. Run `naro login`."
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return session;
|
|
390
|
+
}
|
|
391
|
+
async function runWhoami(context) {
|
|
392
|
+
const session = await currentSession();
|
|
393
|
+
const handle = session.user.github ? ` (${session.user.github})` : "";
|
|
394
|
+
context.output.result(
|
|
395
|
+
{
|
|
396
|
+
user: {
|
|
397
|
+
id: session.user.id,
|
|
398
|
+
email: session.user.email,
|
|
399
|
+
github: session.user.github
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
[`Logged in as ${session.user.email ?? session.user.id}${handle}`]
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
async function runToken(context) {
|
|
406
|
+
const session = await currentSession();
|
|
407
|
+
process.stderr.write(
|
|
408
|
+
"\u26A0 Access tokens expire after about an hour \u2014 generate one per CI run.\n"
|
|
409
|
+
);
|
|
410
|
+
context.output.result(
|
|
411
|
+
{ token: session.access_token, expiresAt: session.expires_at },
|
|
412
|
+
[session.access_token]
|
|
413
|
+
);
|
|
414
|
+
}
|
|
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
|
+
|
|
570
|
+
// src/commands/deploy.ts
|
|
571
|
+
async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDeploy }) {
|
|
572
|
+
const { flags, output } = context;
|
|
573
|
+
const link = flags.project ? { projectId: flags.project } : readProjectLink(context.cwd);
|
|
574
|
+
if (flags.prod && !flags.yes && !flags.json && isInteractive()) {
|
|
575
|
+
const confirmed = await context.prompter.confirm("Deploy to production?");
|
|
576
|
+
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
577
|
+
}
|
|
578
|
+
output.log(`
|
|
579
|
+
\u{1F680} Deploying ${context.cwd}
|
|
580
|
+
`);
|
|
581
|
+
const canPrompt = !flags.json && !flags.yes && isInteractive();
|
|
582
|
+
const result = await deps.deploy(
|
|
583
|
+
context.cwd,
|
|
66
584
|
{
|
|
67
585
|
name: flags.name,
|
|
586
|
+
projectId: link?.projectId,
|
|
68
587
|
rootDirectory: flags.root ?? null,
|
|
69
588
|
branch: flags.branch,
|
|
70
|
-
production: flags.
|
|
71
|
-
|
|
589
|
+
production: flags.prod,
|
|
590
|
+
...canPrompt && {
|
|
591
|
+
onMissingEnv: (keys) => promptForMissingEnv(
|
|
592
|
+
{ cwd: context.cwd, prompter: context.prompter, output },
|
|
593
|
+
keys
|
|
594
|
+
)
|
|
595
|
+
},
|
|
596
|
+
token: flags.token,
|
|
597
|
+
allowLogin: !flags.json && !flags.yes
|
|
72
598
|
},
|
|
73
|
-
(message) =>
|
|
74
|
-
`)
|
|
599
|
+
(message) => output.log(message)
|
|
75
600
|
);
|
|
76
|
-
|
|
77
|
-
|
|
601
|
+
if (!link) {
|
|
602
|
+
writeProjectLink(context.cwd, { projectId: result.projectId });
|
|
603
|
+
if (ensureGitignoreHasNaro(context.cwd))
|
|
604
|
+
output.log("\u25B8 added .naro to .gitignore");
|
|
605
|
+
output.log("\u25B8 linked (.naro/project.json)");
|
|
606
|
+
}
|
|
607
|
+
await deps.offerAutoDeploy?.(context, result.projectId, { canPrompt });
|
|
608
|
+
context.output.result(
|
|
609
|
+
{
|
|
610
|
+
deploymentId: result.deploymentId,
|
|
611
|
+
projectId: result.projectId,
|
|
612
|
+
status: result.status,
|
|
613
|
+
url: result.url,
|
|
614
|
+
target: result.production ? "production" : "preview",
|
|
615
|
+
missingEnvKeys: result.missingEnvKeys
|
|
616
|
+
},
|
|
617
|
+
[
|
|
618
|
+
"",
|
|
619
|
+
`\u2705 Deployment ready (${result.production ? "production" : "preview"})`,
|
|
620
|
+
"",
|
|
621
|
+
result.url
|
|
622
|
+
]
|
|
623
|
+
);
|
|
624
|
+
}
|
|
78
625
|
|
|
79
|
-
|
|
626
|
+
// src/commands/deployments.ts
|
|
627
|
+
function shortSha(sha) {
|
|
628
|
+
return sha ? sha.slice(0, 7) : "-";
|
|
80
629
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
630
|
+
function formatDeploymentLine(deployment) {
|
|
631
|
+
return [
|
|
632
|
+
deployment.id.padEnd(38),
|
|
633
|
+
deployment.status.padEnd(10),
|
|
634
|
+
deployment.branch.padEnd(16),
|
|
635
|
+
shortSha(deployment.gitCommitSha).padEnd(9),
|
|
636
|
+
deployment.url ?? ""
|
|
637
|
+
].join(" ");
|
|
638
|
+
}
|
|
639
|
+
async function runDeployments(context) {
|
|
640
|
+
const client = await context.client();
|
|
641
|
+
const projectId = await context.resolveProjectId();
|
|
642
|
+
const deployments = await client.deployments.list(projectId);
|
|
643
|
+
context.output.result({ projectId, deployments }, [
|
|
644
|
+
`Deployments for ${projectId}:`,
|
|
645
|
+
"",
|
|
646
|
+
[
|
|
647
|
+
"ID".padEnd(38),
|
|
648
|
+
"STATUS".padEnd(10),
|
|
649
|
+
"BRANCH".padEnd(16),
|
|
650
|
+
"COMMIT".padEnd(9),
|
|
651
|
+
"URL"
|
|
652
|
+
].join(" "),
|
|
653
|
+
...deployments.map(formatDeploymentLine)
|
|
654
|
+
]);
|
|
655
|
+
}
|
|
656
|
+
async function runInspect(context) {
|
|
657
|
+
const [deploymentId] = context.positionalOrThrow(
|
|
658
|
+
1,
|
|
659
|
+
"Usage: naro inspect <deployment-id>"
|
|
660
|
+
);
|
|
661
|
+
const client = await context.client();
|
|
662
|
+
const detail = await client.deployments.get(deploymentId);
|
|
663
|
+
const { deployment } = detail;
|
|
664
|
+
const lastLogs = detail.logs.slice(-15);
|
|
665
|
+
context.output.result({ deployment, logs: detail.logs }, [
|
|
666
|
+
`Deployment ${deployment.id}`,
|
|
667
|
+
` status: ${deployment.status}${deployment.errorCode ? ` (${deployment.errorCode})` : ""}`,
|
|
668
|
+
` branch: ${deployment.branch}`,
|
|
669
|
+
` commit: ${deployment.gitCommitSha ?? "-"}`,
|
|
670
|
+
` url: ${deployment.url ?? "-"}`,
|
|
671
|
+
` created: ${deployment.createdAt}`,
|
|
672
|
+
` finished: ${deployment.finishedAt ?? "-"}`,
|
|
673
|
+
...deployment.errorMessage ? [` error: ${deployment.errorMessage}`] : [],
|
|
674
|
+
...lastLogs.length > 0 ? [
|
|
675
|
+
"",
|
|
676
|
+
"Recent build logs:",
|
|
677
|
+
...lastLogs.map((log) => ` [${log.stage}] ${log.message}`)
|
|
678
|
+
] : []
|
|
679
|
+
]);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/commands/confirm.ts
|
|
683
|
+
async function confirmOrAbort(context, question) {
|
|
684
|
+
if (context.flags.yes) return;
|
|
685
|
+
if (context.flags.json || !isInteractive()) {
|
|
686
|
+
throw new NaroError(
|
|
687
|
+
"CONFIRMATION_REQUIRED",
|
|
688
|
+
`${question} Re-run with --yes to confirm.`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
const confirmed = await context.prompter.confirm(question);
|
|
692
|
+
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/commands/domains.ts
|
|
696
|
+
function domainLines(domain) {
|
|
697
|
+
const lines = [
|
|
698
|
+
` ${domain.hostname.padEnd(36)} ${domain.status.padEnd(12)} ssl: ${domain.sslStatus}`
|
|
699
|
+
];
|
|
700
|
+
if (domain.status !== "active" && domain.verification.length > 0) {
|
|
701
|
+
lines.push(" Required DNS records:");
|
|
702
|
+
for (const record of domain.verification) {
|
|
703
|
+
lines.push(
|
|
704
|
+
` Type: ${record.type.toUpperCase().padEnd(6)} Name: ${record.name}`,
|
|
705
|
+
` Value: ${record.value}`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return lines;
|
|
710
|
+
}
|
|
711
|
+
async function runDomainsList(context) {
|
|
712
|
+
const client = await context.client();
|
|
713
|
+
const projectId = await context.resolveProjectId();
|
|
714
|
+
const domains = await client.domains.list(projectId);
|
|
715
|
+
context.output.result(
|
|
716
|
+
{ projectId, domains },
|
|
717
|
+
domains.length === 0 ? ["No custom domains. Add one with `naro domains add example.com`."] : [
|
|
718
|
+
`Domains (${String(domains.length)}):`,
|
|
719
|
+
...domains.flatMap(domainLines)
|
|
720
|
+
]
|
|
86
721
|
);
|
|
87
|
-
|
|
88
|
-
|
|
722
|
+
}
|
|
723
|
+
async function runDomainsAdd(context) {
|
|
724
|
+
const [hostname] = context.positionalOrThrow(
|
|
725
|
+
1,
|
|
726
|
+
"Usage: naro domains add <hostname>"
|
|
727
|
+
);
|
|
728
|
+
const client = await context.client();
|
|
729
|
+
const projectId = await context.resolveProjectId();
|
|
730
|
+
const domain = await client.domains.add(projectId, hostname);
|
|
731
|
+
context.output.result({ projectId, domain }, [
|
|
732
|
+
`\u2705 Domain added: ${domain.hostname}`,
|
|
733
|
+
"",
|
|
734
|
+
...domainLines(domain),
|
|
735
|
+
"",
|
|
736
|
+
domain.status === "active" ? "The domain is active." : "Status: Pending verification \u2014 add the DNS records above, then run `naro domains list` to refresh."
|
|
737
|
+
]);
|
|
738
|
+
}
|
|
739
|
+
async function runDomainsRemove(context) {
|
|
740
|
+
const [hostname] = context.positionalOrThrow(
|
|
741
|
+
1,
|
|
742
|
+
"Usage: naro domains remove <hostname>"
|
|
743
|
+
);
|
|
744
|
+
await confirmOrAbort(
|
|
745
|
+
context,
|
|
746
|
+
`Remove ${String(hostname)}? Traffic to it will stop routing here.`
|
|
747
|
+
);
|
|
748
|
+
const client = await context.client();
|
|
749
|
+
const projectId = await context.resolveProjectId();
|
|
750
|
+
await client.domains.remove(projectId, hostname);
|
|
751
|
+
context.output.result({ projectId, hostname, removed: true }, [
|
|
752
|
+
`\u2705 Removed ${String(hostname)}`
|
|
753
|
+
]);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// src/commands/env.ts
|
|
757
|
+
import { existsSync, writeFileSync } from "fs";
|
|
758
|
+
import { join } from "path";
|
|
759
|
+
var TARGETS = /* @__PURE__ */ new Set(["production", "preview", "all"]);
|
|
760
|
+
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
761
|
+
function parseTarget(value) {
|
|
762
|
+
if (value === void 0) return "all";
|
|
763
|
+
if (!TARGETS.has(value))
|
|
764
|
+
throw new NaroError(
|
|
765
|
+
"VALIDATION_FAILED",
|
|
766
|
+
"Environment must be production, preview or all."
|
|
767
|
+
);
|
|
768
|
+
return value;
|
|
769
|
+
}
|
|
770
|
+
function maskedValue(row) {
|
|
771
|
+
if (!row.isSecret) return row.value ?? "";
|
|
772
|
+
return "\u2022\u2022\u2022\u2022\u2022\u2022 (secret)";
|
|
773
|
+
}
|
|
774
|
+
async function runEnvList(context) {
|
|
775
|
+
const client = await context.client();
|
|
776
|
+
const projectId = await context.resolveProjectId();
|
|
777
|
+
const reveal = context.flags.reveal;
|
|
778
|
+
const rows = await client.env.list(projectId, { reveal });
|
|
779
|
+
context.output.result(
|
|
780
|
+
{
|
|
781
|
+
projectId,
|
|
782
|
+
envVars: rows.map((row) => ({
|
|
783
|
+
id: row.id,
|
|
784
|
+
key: row.key,
|
|
785
|
+
target: row.target,
|
|
786
|
+
isSecret: row.isSecret,
|
|
787
|
+
value: row.isSecret && !reveal ? null : row.value
|
|
788
|
+
}))
|
|
789
|
+
},
|
|
790
|
+
[
|
|
791
|
+
`Environment variables (${String(rows.length)}):`,
|
|
792
|
+
...rows.map(
|
|
793
|
+
(row) => ` ${row.key.padEnd(32)} ${row.target.padEnd(12)} ${reveal ? row.value ?? "" : maskedValue(row)}`
|
|
794
|
+
)
|
|
795
|
+
]
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
async function runEnvAdd(context) {
|
|
799
|
+
context.positionalOrThrow(
|
|
800
|
+
1,
|
|
801
|
+
"Usage: naro env add KEY [production|preview|all]"
|
|
802
|
+
);
|
|
803
|
+
const [key, targetArg] = context.positionals;
|
|
804
|
+
if (!key || !KEY_RE.test(key) || key.length > 256)
|
|
805
|
+
throw new NaroError(
|
|
806
|
+
"VALIDATION_FAILED",
|
|
807
|
+
"Keys must look like MY_VARIABLE (letters, digits, underscore)."
|
|
808
|
+
);
|
|
809
|
+
const target = parseTarget(targetArg);
|
|
810
|
+
let value;
|
|
811
|
+
if (!process.stdin.isTTY) {
|
|
812
|
+
value = await readStdin();
|
|
813
|
+
} else {
|
|
814
|
+
value = await context.prompter.askHidden(`? What's the value of ${key}? `);
|
|
815
|
+
}
|
|
816
|
+
if (value.length === 0)
|
|
817
|
+
throw new NaroError("VALIDATION_FAILED", `No value provided for ${key}.`);
|
|
818
|
+
const isSecret = !key.startsWith("NEXT_PUBLIC_");
|
|
819
|
+
const client = await context.client();
|
|
820
|
+
const projectId = await context.resolveProjectId();
|
|
821
|
+
await client.env.set(projectId, [{ key, value, target, isSecret }]);
|
|
822
|
+
context.output.result({ projectId, key, target, isSecret }, [
|
|
823
|
+
`\u2705 Added ${key} (${target}${isSecret ? ", secret" : ""})`
|
|
824
|
+
]);
|
|
825
|
+
}
|
|
826
|
+
async function runEnvRemove(context) {
|
|
827
|
+
context.positionalOrThrow(
|
|
828
|
+
1,
|
|
829
|
+
"Usage: naro env remove KEY [production|preview|all]"
|
|
830
|
+
);
|
|
831
|
+
const [key, targetArg] = context.positionals;
|
|
832
|
+
const target = targetArg === void 0 ? void 0 : parseTarget(targetArg);
|
|
833
|
+
await confirmOrAbort(
|
|
834
|
+
context,
|
|
835
|
+
`Remove ${String(key)}${target ? ` (${target})` : ""} from this project?`
|
|
836
|
+
);
|
|
837
|
+
const client = await context.client();
|
|
838
|
+
const projectId = await context.resolveProjectId();
|
|
839
|
+
const removed = await client.env.remove(projectId, key, target);
|
|
840
|
+
context.output.result({ projectId, key, removed }, [
|
|
841
|
+
`\u2705 Removed ${String(key)} (${String(removed)} entr${removed === 1 ? "y" : "ies"})`
|
|
842
|
+
]);
|
|
843
|
+
}
|
|
844
|
+
async function runEnvPull(context) {
|
|
845
|
+
const file = context.positionals[0] ?? ".env.local";
|
|
846
|
+
const surface = context.flags.env === "production" ? "production" : "preview";
|
|
847
|
+
if (context.flags.env && !["production", "preview"].includes(context.flags.env))
|
|
848
|
+
throw new NaroError(
|
|
849
|
+
"VALIDATION_FAILED",
|
|
850
|
+
"--env must be production or preview."
|
|
851
|
+
);
|
|
852
|
+
const client = await context.client();
|
|
853
|
+
const projectId = await context.resolveProjectId();
|
|
854
|
+
const rows = await client.env.list(projectId, { reveal: true });
|
|
855
|
+
const selected = rows.filter(
|
|
856
|
+
(row) => row.target === "all" || row.target === surface
|
|
857
|
+
);
|
|
858
|
+
const vars = Object.fromEntries(
|
|
859
|
+
selected.map((row) => [row.key, row.value ?? ""])
|
|
860
|
+
);
|
|
861
|
+
const path = join(context.cwd, file);
|
|
862
|
+
if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
|
|
863
|
+
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
864
|
+
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
865
|
+
}
|
|
866
|
+
writeFileSync(
|
|
867
|
+
path,
|
|
868
|
+
`# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
869
|
+
${serializeDotenv(vars)}`,
|
|
870
|
+
{ mode: 384 }
|
|
871
|
+
);
|
|
872
|
+
context.output.result(
|
|
873
|
+
{ projectId, file, environment: surface, count: selected.length },
|
|
874
|
+
[`\u2705 Pulled ${String(selected.length)} variable(s) into ${file}`]
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/commands/link.ts
|
|
879
|
+
async function resolveTeam(context, teams) {
|
|
880
|
+
if (context.flags.team) {
|
|
881
|
+
const match = teams.find((team) => team.slug === context.flags.team);
|
|
882
|
+
if (!match)
|
|
883
|
+
throw new NaroError(
|
|
884
|
+
"NOT_FOUND",
|
|
885
|
+
`You are not a member of team "${context.flags.team}".`
|
|
886
|
+
);
|
|
887
|
+
return match;
|
|
888
|
+
}
|
|
889
|
+
const linked = context.projectLink();
|
|
890
|
+
if (linked?.teamSlug) {
|
|
891
|
+
const match = teams.find((team) => team.slug === linked.teamSlug);
|
|
892
|
+
if (match) return match;
|
|
893
|
+
}
|
|
894
|
+
const personal = teams.find((team) => team.isPersonal);
|
|
895
|
+
const fallback = personal ?? teams[0];
|
|
896
|
+
if (!fallback)
|
|
897
|
+
throw new NaroError("NOT_FOUND", "Your account has no teams yet.");
|
|
898
|
+
if (teams.length > 1 && !personal) {
|
|
899
|
+
const index = await context.prompter.select(
|
|
900
|
+
"Which team?",
|
|
901
|
+
teams.map((team) => `${team.name} (${team.slug})`)
|
|
902
|
+
);
|
|
903
|
+
return teams[index] ?? fallback;
|
|
904
|
+
}
|
|
905
|
+
return fallback;
|
|
906
|
+
}
|
|
907
|
+
function saveLink(context, project, teamSlug) {
|
|
908
|
+
writeProjectLink(context.cwd, {
|
|
909
|
+
projectId: project.id,
|
|
910
|
+
teamSlug,
|
|
911
|
+
projectName: project.name
|
|
912
|
+
});
|
|
913
|
+
if (ensureGitignoreHasNaro(context.cwd))
|
|
914
|
+
context.output.log("\u25B8 added .naro to .gitignore");
|
|
915
|
+
}
|
|
916
|
+
async function runLink(context) {
|
|
917
|
+
const client = await context.client();
|
|
918
|
+
const teams = await client.teams.list();
|
|
919
|
+
const team = await resolveTeam(context, teams);
|
|
920
|
+
const projects = await client.projects.list(team.slug);
|
|
921
|
+
let project;
|
|
922
|
+
if (context.flags.project) {
|
|
923
|
+
project = projects.find(
|
|
924
|
+
(candidate) => candidate.id === context.flags.project
|
|
925
|
+
);
|
|
926
|
+
if (!project) {
|
|
927
|
+
project = await client.projects.get(context.flags.project);
|
|
928
|
+
}
|
|
929
|
+
} else if (context.flags.name) {
|
|
930
|
+
project = projects.find(
|
|
931
|
+
(candidate) => candidate.name === context.flags.name || candidate.slug === context.flags.name
|
|
932
|
+
);
|
|
933
|
+
if (!project)
|
|
934
|
+
throw new NaroError(
|
|
935
|
+
"PROJECT_NOT_FOUND",
|
|
936
|
+
`No project named "${context.flags.name}" in team ${team.slug}. Run \`naro deploy\` to create it.`
|
|
937
|
+
);
|
|
938
|
+
} else {
|
|
939
|
+
const guess = slugify(context.cwd.split("/").at(-1) ?? "app");
|
|
940
|
+
const exact = projects.find(
|
|
941
|
+
(candidate) => candidate.name === guess || candidate.slug === guess
|
|
942
|
+
);
|
|
943
|
+
if (exact && (context.flags.yes || context.flags.json)) {
|
|
944
|
+
project = exact;
|
|
945
|
+
} else if (projects.length === 0) {
|
|
946
|
+
throw new NaroError(
|
|
947
|
+
"PROJECT_NOT_FOUND",
|
|
948
|
+
`Team ${team.slug} has no projects yet. Run \`naro deploy\` to create one.`
|
|
949
|
+
);
|
|
950
|
+
} else {
|
|
951
|
+
const options = projects.map(
|
|
952
|
+
(candidate) => `${candidate.name} (${candidate.subdomain})`
|
|
953
|
+
);
|
|
954
|
+
const index = await context.prompter.select(
|
|
955
|
+
`Link ${context.cwd} to which project?`,
|
|
956
|
+
options
|
|
957
|
+
);
|
|
958
|
+
project = projects[index];
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (!project)
|
|
962
|
+
throw new NaroError("PROJECT_NOT_FOUND", "No project selected.");
|
|
963
|
+
const owningTeam = teams.find((candidate) => candidate.id === project.teamId) ?? team;
|
|
964
|
+
saveLink(context, project, owningTeam.slug);
|
|
965
|
+
context.output.result(
|
|
966
|
+
{
|
|
967
|
+
projectId: project.id,
|
|
968
|
+
projectName: project.name,
|
|
969
|
+
teamSlug: owningTeam.slug,
|
|
970
|
+
subdomain: project.subdomain
|
|
971
|
+
},
|
|
972
|
+
[`\u2705 Linked to ${owningTeam.slug}/${project.name} (.naro/project.json)`]
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
async function runUnlink(context) {
|
|
976
|
+
const existing = readProjectLink(context.cwd);
|
|
977
|
+
const removed = removeProjectLink(context.cwd);
|
|
978
|
+
context.output.result(
|
|
979
|
+
{ removed, ...existing ? { projectId: existing.projectId } : {} },
|
|
980
|
+
[removed ? "\u2705 Unlinked (.naro removed)" : "Nothing to unlink here."]
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// src/commands/logs.ts
|
|
985
|
+
var FOLLOW_INTERVAL_MS = 3e3;
|
|
986
|
+
function sleep(ms) {
|
|
987
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
988
|
+
}
|
|
989
|
+
function formatBuildLog(log) {
|
|
990
|
+
return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
|
|
991
|
+
}
|
|
992
|
+
function formatRuntimeLog(entry) {
|
|
993
|
+
const time = new Date(entry.ts).toISOString();
|
|
994
|
+
const request = entry.method && entry.path ? ` ${entry.method} ${entry.path}${entry.status ? ` ${String(entry.status)}` : ""}` : "";
|
|
995
|
+
return `${time} [${entry.level}]${request} ${entry.message}`.trimEnd();
|
|
996
|
+
}
|
|
997
|
+
async function latestProductionDeploymentId(client, projectId) {
|
|
998
|
+
const deployments = await client.deployments.list(projectId);
|
|
999
|
+
const latest = deployments.find(
|
|
1000
|
+
(deployment) => deployment.status === "READY"
|
|
1001
|
+
);
|
|
1002
|
+
const fallback = deployments[0];
|
|
1003
|
+
const chosen = latest ?? fallback;
|
|
1004
|
+
if (!chosen)
|
|
1005
|
+
throw new NaroError(
|
|
1006
|
+
"DEPLOYMENT_NOT_FOUND",
|
|
1007
|
+
"This project has no deployments yet."
|
|
1008
|
+
);
|
|
1009
|
+
return chosen.id;
|
|
1010
|
+
}
|
|
1011
|
+
async function buildLogs(context, client, deploymentId) {
|
|
1012
|
+
if (!context.flags.follow) {
|
|
1013
|
+
const detail = await client.deployments.get(deploymentId);
|
|
1014
|
+
context.output.result(
|
|
1015
|
+
{ deploymentId, type: "build", logs: detail.logs },
|
|
1016
|
+
detail.logs.map(formatBuildLog)
|
|
1017
|
+
);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
let lastSequence = -1;
|
|
1021
|
+
for (; ; ) {
|
|
1022
|
+
const detail = await client.deployments.get(deploymentId);
|
|
1023
|
+
for (const log of detail.logs) {
|
|
1024
|
+
if (log.sequence > lastSequence) {
|
|
1025
|
+
lastSequence = log.sequence;
|
|
1026
|
+
context.output.log(formatBuildLog(log));
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (isTerminalStatus(detail.deployment.status)) {
|
|
1030
|
+
context.output.result(
|
|
1031
|
+
{
|
|
1032
|
+
deploymentId,
|
|
1033
|
+
type: "build",
|
|
1034
|
+
status: detail.deployment.status,
|
|
1035
|
+
logs: detail.logs
|
|
1036
|
+
},
|
|
1037
|
+
[`\u2014 deployment ${detail.deployment.status} \u2014`]
|
|
1038
|
+
);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
await sleep(FOLLOW_INTERVAL_MS);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
async function runtimeLogs(context, client, projectId) {
|
|
1045
|
+
const environment = context.flags.env === "preview" ? "preview" : "production";
|
|
1046
|
+
if (context.flags.env && !["production", "preview"].includes(context.flags.env))
|
|
1047
|
+
throw new NaroError(
|
|
1048
|
+
"VALIDATION_FAILED",
|
|
1049
|
+
"--env must be production or preview."
|
|
1050
|
+
);
|
|
1051
|
+
const options = {
|
|
1052
|
+
environment,
|
|
1053
|
+
...context.flags.level ? { level: context.flags.level } : {}
|
|
1054
|
+
};
|
|
1055
|
+
if (!context.flags.follow) {
|
|
1056
|
+
const entries = await client.logs.runtime(projectId, options);
|
|
1057
|
+
context.output.result(
|
|
1058
|
+
{ projectId, type: "runtime", environment, entries },
|
|
1059
|
+
entries.length > 0 ? entries.map(formatRuntimeLog) : ["(no runtime logs yet \u2014 the worker logs on each request)"]
|
|
1060
|
+
);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
context.output.log(
|
|
1064
|
+
`Following ${environment} runtime logs (Ctrl-C to stop)...`
|
|
1065
|
+
);
|
|
1066
|
+
let lastTs = -1;
|
|
1067
|
+
let boundary = /* @__PURE__ */ new Set();
|
|
1068
|
+
for (; ; ) {
|
|
1069
|
+
const entries = await client.logs.runtime(projectId, options);
|
|
1070
|
+
for (const entry of entries) {
|
|
1071
|
+
if (entry.ts < lastTs) continue;
|
|
1072
|
+
const key = JSON.stringify(entry);
|
|
1073
|
+
if (entry.ts === lastTs) {
|
|
1074
|
+
if (boundary.has(key)) continue;
|
|
1075
|
+
} else {
|
|
1076
|
+
lastTs = entry.ts;
|
|
1077
|
+
boundary = /* @__PURE__ */ new Set();
|
|
1078
|
+
}
|
|
1079
|
+
boundary.add(key);
|
|
1080
|
+
context.output.log(formatRuntimeLog(entry));
|
|
1081
|
+
}
|
|
1082
|
+
await sleep(FOLLOW_INTERVAL_MS);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
async function runLogs(context) {
|
|
1086
|
+
const client = await context.client();
|
|
1087
|
+
const [deploymentId] = context.positionals;
|
|
1088
|
+
const type = context.flags.type ?? (deploymentId !== void 0 ? "build" : "runtime");
|
|
1089
|
+
if (type !== "build" && type !== "runtime")
|
|
1090
|
+
throw new NaroError(
|
|
1091
|
+
"VALIDATION_FAILED",
|
|
1092
|
+
"--type must be build or runtime."
|
|
1093
|
+
);
|
|
1094
|
+
if (type === "runtime") {
|
|
1095
|
+
if (deploymentId)
|
|
1096
|
+
throw new NaroError(
|
|
1097
|
+
"VALIDATION_FAILED",
|
|
1098
|
+
"Runtime logs are per project, not per deployment. Drop the deployment id or use --type build."
|
|
1099
|
+
);
|
|
1100
|
+
const projectId = await context.resolveProjectId();
|
|
1101
|
+
await runtimeLogs(context, client, projectId);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const target = deploymentId ?? await latestProductionDeploymentId(
|
|
1105
|
+
client,
|
|
1106
|
+
await context.resolveProjectId()
|
|
1107
|
+
);
|
|
1108
|
+
await buildLogs(context, client, target);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/commands/projects.ts
|
|
1112
|
+
function productionUrl(subdomain) {
|
|
1113
|
+
return `https://${subdomain}.${new URL(controlPlaneUrl()).hostname}`;
|
|
1114
|
+
}
|
|
1115
|
+
async function runProjects(context) {
|
|
1116
|
+
const client = await context.client();
|
|
1117
|
+
const teams = await client.teams.list();
|
|
1118
|
+
const team = await resolveTeam(context, teams);
|
|
1119
|
+
const projects = await client.projects.list(team.slug);
|
|
1120
|
+
context.output.result(
|
|
1121
|
+
{
|
|
1122
|
+
team: team.slug,
|
|
1123
|
+
projects: projects.map((project) => ({
|
|
1124
|
+
id: project.id,
|
|
1125
|
+
name: project.name,
|
|
1126
|
+
subdomain: project.subdomain,
|
|
1127
|
+
url: productionUrl(project.subdomain),
|
|
1128
|
+
defaultBranch: project.defaultBranch,
|
|
1129
|
+
updatedAt: project.updatedAt
|
|
1130
|
+
}))
|
|
1131
|
+
},
|
|
1132
|
+
[
|
|
1133
|
+
`Projects in ${team.slug} (${String(projects.length)}):`,
|
|
1134
|
+
...projects.map(
|
|
1135
|
+
(project) => ` ${project.name.padEnd(24)} ${productionUrl(project.subdomain).padEnd(40)} ${project.id}`
|
|
1136
|
+
)
|
|
1137
|
+
]
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
async function runProjectInspect(context) {
|
|
1141
|
+
const client = await context.client();
|
|
1142
|
+
const projectId = await context.resolveProjectId();
|
|
1143
|
+
const project = await client.projects.get(projectId);
|
|
1144
|
+
const url = productionUrl(project.subdomain);
|
|
1145
|
+
context.output.result({ project: { ...project, url } }, [
|
|
1146
|
+
`${project.name}`,
|
|
1147
|
+
` id: ${project.id}`,
|
|
1148
|
+
` team: ${project.teamId}`,
|
|
1149
|
+
` production URL: ${url}`,
|
|
1150
|
+
` preview URL: https://${project.subdomain}-preview.${new URL(controlPlaneUrl()).hostname}`,
|
|
1151
|
+
` default branch: ${project.defaultBranch}`,
|
|
1152
|
+
` root directory: ${project.rootDirectory ?? "(repo root)"}`,
|
|
1153
|
+
` repository: ${project.repositoryFullName ?? project.repositoryUrl}`
|
|
1154
|
+
]);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/commands/promote.ts
|
|
1158
|
+
async function promoteAndWait(context, client, project, sourceDeploymentId, action) {
|
|
1159
|
+
const promoted = await client.deployments.promote(
|
|
1160
|
+
project.id,
|
|
1161
|
+
sourceDeploymentId
|
|
1162
|
+
);
|
|
1163
|
+
context.output.log(
|
|
1164
|
+
`\u25B8 ${action === "promote" ? "promoting" : "rolling back to"} ${sourceDeploymentId}`
|
|
1165
|
+
);
|
|
1166
|
+
const detail = await client.deployments.wait(promoted.id, {
|
|
1167
|
+
timeoutMs: 5 * 6e4
|
|
1168
|
+
});
|
|
1169
|
+
if (detail.deployment.status !== "READY") {
|
|
1170
|
+
throw new NaroError(
|
|
1171
|
+
detail.deployment.errorCode ?? "DEPLOY_FAILED",
|
|
1172
|
+
detail.deployment.errorMessage ?? `The ${action} failed.`,
|
|
1173
|
+
{ details: { deploymentId: promoted.id } }
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
context.output.result(
|
|
1177
|
+
{
|
|
1178
|
+
deploymentId: promoted.id,
|
|
1179
|
+
sourceDeploymentId,
|
|
1180
|
+
projectId: project.id,
|
|
1181
|
+
status: detail.deployment.status,
|
|
1182
|
+
url: detail.deployment.url
|
|
1183
|
+
},
|
|
1184
|
+
[
|
|
1185
|
+
`\u2705 Production now serves ${sourceDeploymentId}`,
|
|
1186
|
+
"",
|
|
1187
|
+
detail.deployment.url ?? ""
|
|
1188
|
+
]
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
async function runPromote(context) {
|
|
1192
|
+
const [deploymentId] = context.positionalOrThrow(
|
|
1193
|
+
1,
|
|
1194
|
+
"Usage: naro promote <deployment-id>"
|
|
1195
|
+
);
|
|
1196
|
+
const client = await context.client();
|
|
1197
|
+
const projectId = await context.resolveProjectId();
|
|
1198
|
+
const project = await client.projects.get(projectId);
|
|
1199
|
+
await confirmOrAbort(
|
|
1200
|
+
context,
|
|
1201
|
+
`Promote ${String(deploymentId)} to production (${project.subdomain})?`
|
|
1202
|
+
);
|
|
1203
|
+
await promoteAndWait(
|
|
1204
|
+
context,
|
|
1205
|
+
client,
|
|
1206
|
+
project,
|
|
1207
|
+
deploymentId,
|
|
1208
|
+
"promote"
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
async function runRollback(context) {
|
|
1212
|
+
const client = await context.client();
|
|
1213
|
+
const projectId = await context.resolveProjectId();
|
|
1214
|
+
const project = await client.projects.get(projectId);
|
|
1215
|
+
let target = context.positionals[0];
|
|
1216
|
+
if (!target) {
|
|
1217
|
+
const { current, target: candidate } = await client.deployments.rollbackTarget(projectId, project.defaultBranch);
|
|
1218
|
+
if (!candidate)
|
|
1219
|
+
throw new NaroError(
|
|
1220
|
+
"DEPLOYMENT_NOT_FOUND",
|
|
1221
|
+
"No earlier production build with a different artifact to roll back to. Pass a deployment id explicitly."
|
|
1222
|
+
);
|
|
1223
|
+
context.output.log(
|
|
1224
|
+
`\u25B8 current production: ${current.id}${current.gitCommitSha ? ` (${current.gitCommitSha.slice(0, 7)})` : ""}`
|
|
1225
|
+
);
|
|
1226
|
+
target = candidate.id;
|
|
1227
|
+
}
|
|
1228
|
+
await confirmOrAbort(
|
|
1229
|
+
context,
|
|
1230
|
+
`Are you sure you want to rollback production to ${target}?`
|
|
1231
|
+
);
|
|
1232
|
+
await promoteAndWait(context, client, project, target, "rollback");
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/router.ts
|
|
1236
|
+
var HELP = `naro \u2014 deploy Next.js apps to Naro (https://naro.sh)
|
|
1237
|
+
|
|
1238
|
+
Usage:
|
|
1239
|
+
naro login | logout | whoami
|
|
1240
|
+
naro token Print an access token for CI (NARO_TOKEN)
|
|
1241
|
+
|
|
1242
|
+
naro link | init Link this directory to a Naro project
|
|
1243
|
+
naro unlink Remove the link (.naro/)
|
|
1244
|
+
naro projects [--team <slug>] List projects
|
|
1245
|
+
naro project inspect Show the linked project
|
|
1246
|
+
|
|
1247
|
+
naro deploy [--prod] Build locally and deploy (preview by default)
|
|
1248
|
+
naro deployments List deployments of the linked project
|
|
1249
|
+
naro inspect <deployment-id> Show one deployment + build logs
|
|
1250
|
+
|
|
1251
|
+
naro logs [deployment-id] Runtime logs (or build logs of a deployment)
|
|
1252
|
+
[--type build|runtime] [--env production|preview] [--level error] [--follow]
|
|
1253
|
+
|
|
1254
|
+
naro env list List env vars (secrets masked)
|
|
1255
|
+
naro env add KEY [environment] Add/update (value from prompt or stdin)
|
|
1256
|
+
naro env remove KEY [environment] Remove
|
|
1257
|
+
naro env pull [file] Write env vars to .env.local
|
|
1258
|
+
|
|
1259
|
+
naro domains list | add <host> | remove <host>
|
|
1260
|
+
|
|
1261
|
+
naro promote <deployment-id> Publish a build to production
|
|
1262
|
+
naro rollback [deployment-id] Roll production back to the previous build
|
|
1263
|
+
|
|
1264
|
+
Global options:
|
|
1265
|
+
--json Machine-readable output (single JSON object on stdout)
|
|
1266
|
+
--yes, -y Never prompt; assume yes (CI / agents)
|
|
1267
|
+
--token <t> Access token (or set NARO_TOKEN)
|
|
1268
|
+
--project <id> Target project (overrides .naro/project.json)
|
|
1269
|
+
--team <slug> Team scope for project listing/linking
|
|
1270
|
+
--cwd <dir> Run as if started in <dir>
|
|
1271
|
+
--prod Deploy/target production
|
|
1272
|
+
--debug Verbose diagnostics
|
|
1273
|
+
|
|
1274
|
+
Credentials: \`naro login\` opens a browser (GitHub). CI: NARO_TOKEN=$(naro token).
|
|
1275
|
+
Set NARO_CONTROL_PLANE_URL to use a custom control plane (default https://naro.sh).
|
|
1276
|
+
`;
|
|
1277
|
+
function route(parsed) {
|
|
1278
|
+
const { command, positionals } = parsed;
|
|
1279
|
+
const sub = positionals[0];
|
|
1280
|
+
switch (command) {
|
|
1281
|
+
case "login":
|
|
1282
|
+
return { handler: runLogin, positionals };
|
|
1283
|
+
case "logout":
|
|
1284
|
+
return { handler: runLogout, positionals };
|
|
1285
|
+
case "whoami":
|
|
1286
|
+
return { handler: runWhoami, positionals };
|
|
1287
|
+
case "token":
|
|
1288
|
+
return { handler: runToken, positionals };
|
|
1289
|
+
case "link":
|
|
1290
|
+
case "init":
|
|
1291
|
+
return { handler: runLink, positionals };
|
|
1292
|
+
case "unlink":
|
|
1293
|
+
return { handler: runUnlink, positionals };
|
|
1294
|
+
case "projects":
|
|
1295
|
+
return { handler: runProjects, positionals };
|
|
1296
|
+
case "project":
|
|
1297
|
+
if (sub === void 0 || sub === "inspect")
|
|
1298
|
+
return {
|
|
1299
|
+
handler: runProjectInspect,
|
|
1300
|
+
positionals: positionals.slice(1)
|
|
1301
|
+
};
|
|
1302
|
+
return null;
|
|
1303
|
+
case "deploy":
|
|
1304
|
+
return { handler: runDeploy, positionals };
|
|
1305
|
+
case "deployments":
|
|
1306
|
+
case "ls":
|
|
1307
|
+
return { handler: runDeployments, positionals };
|
|
1308
|
+
case "inspect":
|
|
1309
|
+
return { handler: runInspect, positionals };
|
|
1310
|
+
case "logs":
|
|
1311
|
+
return { handler: runLogs, positionals };
|
|
1312
|
+
case "env":
|
|
1313
|
+
switch (sub) {
|
|
1314
|
+
case void 0:
|
|
1315
|
+
case "list":
|
|
1316
|
+
case "ls":
|
|
1317
|
+
return { handler: runEnvList, positionals: positionals.slice(1) };
|
|
1318
|
+
case "add":
|
|
1319
|
+
return { handler: runEnvAdd, positionals: positionals.slice(1) };
|
|
1320
|
+
case "remove":
|
|
1321
|
+
case "rm":
|
|
1322
|
+
return { handler: runEnvRemove, positionals: positionals.slice(1) };
|
|
1323
|
+
case "pull":
|
|
1324
|
+
return { handler: runEnvPull, positionals: positionals.slice(1) };
|
|
1325
|
+
default:
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
case "domains":
|
|
1329
|
+
switch (sub) {
|
|
1330
|
+
case void 0:
|
|
1331
|
+
case "list":
|
|
1332
|
+
case "ls":
|
|
1333
|
+
return { handler: runDomainsList, positionals: positionals.slice(1) };
|
|
1334
|
+
case "add":
|
|
1335
|
+
return { handler: runDomainsAdd, positionals: positionals.slice(1) };
|
|
1336
|
+
case "remove":
|
|
1337
|
+
case "rm":
|
|
1338
|
+
return {
|
|
1339
|
+
handler: runDomainsRemove,
|
|
1340
|
+
positionals: positionals.slice(1)
|
|
1341
|
+
};
|
|
1342
|
+
default:
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
case "promote":
|
|
1346
|
+
return { handler: runPromote, positionals };
|
|
1347
|
+
case "rollback":
|
|
1348
|
+
return { handler: runRollback, positionals };
|
|
1349
|
+
case "git":
|
|
1350
|
+
switch (positionals[0]) {
|
|
1351
|
+
case "connect":
|
|
1352
|
+
return { handler: runGitConnect, positionals: positionals.slice(1) };
|
|
1353
|
+
case "disconnect":
|
|
1354
|
+
return { handler: runGitDisconnect, positionals: [] };
|
|
1355
|
+
default:
|
|
1356
|
+
return null;
|
|
1357
|
+
}
|
|
1358
|
+
default:
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function version() {
|
|
1363
|
+
try {
|
|
1364
|
+
const pkg = JSON.parse(
|
|
1365
|
+
readFileSync(
|
|
1366
|
+
join2(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
|
|
1367
|
+
"utf8"
|
|
1368
|
+
)
|
|
1369
|
+
);
|
|
1370
|
+
return pkg.version ?? "unknown";
|
|
1371
|
+
} catch {
|
|
1372
|
+
return "unknown";
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// src/index.ts
|
|
1377
|
+
async function main(argv = process.argv.slice(2)) {
|
|
1378
|
+
const parsed = parseArgs(argv);
|
|
1379
|
+
if (parsed.flags.version) {
|
|
1380
|
+
process.stdout.write(`${version()}
|
|
1381
|
+
`);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (!parsed.command || parsed.command === "help" || parsed.flags.help) {
|
|
1385
|
+
if (parsed.flags.json) {
|
|
1386
|
+
process.stdout.write(
|
|
1387
|
+
`${JSON.stringify({ success: true, help: HELP }, null, 2)}
|
|
1388
|
+
`
|
|
1389
|
+
);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
process.stdout.write(HELP);
|
|
1393
|
+
if (parsed.command === void 0 && !parsed.flags.help && argv.length > 0)
|
|
1394
|
+
process.exitCode = 1;
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
const output = new Output(parsed.flags.json, parsed.flags.debug);
|
|
1398
|
+
if (parsed.unknown.length > 0) {
|
|
1399
|
+
output.fail(
|
|
1400
|
+
new NaroError(
|
|
1401
|
+
"VALIDATION_FAILED",
|
|
1402
|
+
`Unknown option: ${parsed.unknown.join(", ")}. See \`naro help\`.`
|
|
1403
|
+
)
|
|
1404
|
+
);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const routed = route(parsed);
|
|
1408
|
+
if (!routed) {
|
|
1409
|
+
output.fail(
|
|
1410
|
+
new NaroError(
|
|
1411
|
+
"VALIDATION_FAILED",
|
|
1412
|
+
`Unknown command: ${[parsed.command, ...parsed.positionals].join(" ")}. See \`naro help\`.`
|
|
1413
|
+
)
|
|
1414
|
+
);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const context = createContext(parsed.flags, routed.positionals);
|
|
1418
|
+
try {
|
|
1419
|
+
await routed.handler(context);
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
context.output.fail(error);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
void main();
|
|
89
1425
|
export {
|
|
90
|
-
|
|
1426
|
+
main
|
|
91
1427
|
};
|