@newly/cli 0.1.0 → 0.1.2
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 +69 -10
- package/dist/api.js +26 -0
- package/dist/commands/build.js +250 -0
- package/dist/commands/create.js +1 -1
- package/dist/commands/creds.js +103 -0
- package/dist/commands/deploy.js +55 -16
- package/dist/commands/dns.js +185 -0
- package/dist/commands/domains.js +201 -0
- package/dist/commands/email.js +86 -0
- package/dist/commands/login.js +27 -3
- package/dist/commands/misc.js +38 -11
- package/dist/commands/run-cloud.js +272 -0
- package/dist/commands/services.js +39 -0
- package/dist/commands/stack.js +13 -11
- package/dist/commands/status.js +10 -10
- package/dist/commands/website.js +218 -0
- package/dist/config.js +31 -5
- package/dist/diagnose.js +159 -0
- package/dist/index.js +16 -2
- package/dist/newly-dev.js +10 -0
- package/dist/run-cloud.js +10 -0
- package/dist/services-manifest.js +63 -0
- package/package.json +15 -4
- package/dist/commands/up.js +0 -85
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { ApiClient, friendlyApiError } from '../api.js';
|
|
4
|
+
import { requireCredentials, resolveProject } from '../config.js';
|
|
5
|
+
function makeClient(project) {
|
|
6
|
+
const creds = requireCredentials();
|
|
7
|
+
return {
|
|
8
|
+
client: new ApiClient(creds.apiUrl, creds.token),
|
|
9
|
+
projectId: resolveProject(process.cwd(), project),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export async function resolveDomain(client, projectId, domainNameOrId) {
|
|
13
|
+
const result = (await client.get(`/projects/${projectId}/domains`));
|
|
14
|
+
const domains = result.domains ?? [];
|
|
15
|
+
const domain = domains.find((item) => item.id === domainNameOrId ||
|
|
16
|
+
item.hostname === domainNameOrId ||
|
|
17
|
+
item.root_domain === domainNameOrId);
|
|
18
|
+
if (!domain)
|
|
19
|
+
throw new Error(`Domain ${domainNameOrId} is not connected to this project.`);
|
|
20
|
+
return domain;
|
|
21
|
+
}
|
|
22
|
+
export function renderRecord(record) {
|
|
23
|
+
const ttl = record.ttl === undefined ? '' : ` ttl=${record.ttl}`;
|
|
24
|
+
const proxied = record.proxied === undefined ? '' : ` proxied=${record.proxied}`;
|
|
25
|
+
const priority = record.priority === undefined ? '' : ` priority=${record.priority}`;
|
|
26
|
+
return `${record.id} ${record.type} ${record.name} ${record.content}${ttl}${proxied}${priority}`;
|
|
27
|
+
}
|
|
28
|
+
async function confirm(prompt) {
|
|
29
|
+
const rl = createInterface({ input, output });
|
|
30
|
+
try {
|
|
31
|
+
const answer = (await rl.question(`${prompt} Type "yes" to continue: `)).trim().toLowerCase();
|
|
32
|
+
return answer === 'yes';
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
rl.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function numberOrUndefined(value, optionName = 'number') {
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
const parsed = Number(trimmed);
|
|
43
|
+
if (!trimmed) {
|
|
44
|
+
throw new Error(`${optionName} must be a finite number.`);
|
|
45
|
+
}
|
|
46
|
+
if (!Number.isFinite(parsed)) {
|
|
47
|
+
throw new Error(`${optionName} must be a finite number.`);
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
export async function runDnsList(client, projectId, domainName, log = console.log) {
|
|
52
|
+
const domain = await resolveDomain(client, projectId, domainName);
|
|
53
|
+
const result = (await client.get(`/projects/${projectId}/domains/${domain.id}/dns-records`));
|
|
54
|
+
if (!result.editable) {
|
|
55
|
+
log(JSON.stringify(result.instructions, null, 2));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const records = result.records ?? [];
|
|
59
|
+
log(records.length ? records.map(renderRecord).join('\n') : 'No DNS records.');
|
|
60
|
+
}
|
|
61
|
+
export async function runDnsAdd(client, projectId, domainName, opts, log = console.log) {
|
|
62
|
+
const ttl = numberOrUndefined(opts.ttl, 'ttl');
|
|
63
|
+
const priority = numberOrUndefined(opts.priority, 'priority');
|
|
64
|
+
const domain = await resolveDomain(client, projectId, domainName);
|
|
65
|
+
const result = await client.post(`/projects/${projectId}/domains/${domain.id}/dns-records`, {
|
|
66
|
+
type: opts.type,
|
|
67
|
+
name: opts.name,
|
|
68
|
+
content: opts.content,
|
|
69
|
+
ttl,
|
|
70
|
+
proxied: opts.proxied,
|
|
71
|
+
priority,
|
|
72
|
+
comment: opts.comment,
|
|
73
|
+
});
|
|
74
|
+
log(renderRecord(result.record));
|
|
75
|
+
}
|
|
76
|
+
export async function runDnsEdit(client, projectId, domainName, recordId, opts, log = console.log) {
|
|
77
|
+
const body = {
|
|
78
|
+
type: opts.type,
|
|
79
|
+
name: opts.name,
|
|
80
|
+
content: opts.content,
|
|
81
|
+
ttl: numberOrUndefined(opts.ttl, 'ttl'),
|
|
82
|
+
proxied: opts.proxied,
|
|
83
|
+
priority: numberOrUndefined(opts.priority, 'priority'),
|
|
84
|
+
comment: opts.comment,
|
|
85
|
+
};
|
|
86
|
+
if (Object.values(body).every((value) => value === undefined)) {
|
|
87
|
+
throw new Error('Nothing to edit. Pass at least one record field.');
|
|
88
|
+
}
|
|
89
|
+
const domain = await resolveDomain(client, projectId, domainName);
|
|
90
|
+
const result = await client.patch(`/projects/${projectId}/domains/${domain.id}/dns-records/${recordId}`, body);
|
|
91
|
+
log(renderRecord(result.record));
|
|
92
|
+
}
|
|
93
|
+
export async function runDnsDelete(client, projectId, domainName, recordId, opts, log = console.log, confirmDelete = confirm) {
|
|
94
|
+
const domain = await resolveDomain(client, projectId, domainName);
|
|
95
|
+
const ok = opts.yes || (await confirmDelete(`Delete DNS record ${recordId} from ${domain.hostname}?`));
|
|
96
|
+
if (!ok) {
|
|
97
|
+
log('Cancelled.');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await client.delete(`/projects/${projectId}/domains/${domain.id}/dns-records/${recordId}`, {
|
|
101
|
+
confirm: true,
|
|
102
|
+
});
|
|
103
|
+
log(`Deleted ${recordId}.`);
|
|
104
|
+
}
|
|
105
|
+
export function registerDns(program) {
|
|
106
|
+
const dns = program.command('dns').description('Manage DNS records through Newly backend');
|
|
107
|
+
dns
|
|
108
|
+
.command('list')
|
|
109
|
+
.argument('<domain>')
|
|
110
|
+
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
111
|
+
.description('List DNS records for a managed domain')
|
|
112
|
+
.action(async (domainName, opts) => {
|
|
113
|
+
try {
|
|
114
|
+
const { client, projectId } = makeClient(opts.project);
|
|
115
|
+
await runDnsList(client, projectId, domainName);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
console.error(friendlyApiError(e));
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
dns
|
|
123
|
+
.command('add')
|
|
124
|
+
.argument('<domain>')
|
|
125
|
+
.requiredOption('--type <type>', 'record type')
|
|
126
|
+
.requiredOption('--name <name>', 'record name, or @ for apex')
|
|
127
|
+
.requiredOption('--content <content>', 'record content')
|
|
128
|
+
.option('--ttl <seconds>', 'record TTL', '300')
|
|
129
|
+
.option('--proxied', 'enable Cloudflare proxying')
|
|
130
|
+
.option('--priority <n>', 'MX priority')
|
|
131
|
+
.option('--comment <text>', 'record comment')
|
|
132
|
+
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
133
|
+
.description('Create a DNS record for a managed domain')
|
|
134
|
+
.action(async (domainName, opts) => {
|
|
135
|
+
try {
|
|
136
|
+
const { client, projectId } = makeClient(opts.project);
|
|
137
|
+
await runDnsAdd(client, projectId, domainName, opts);
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
console.error(friendlyApiError(e));
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
dns
|
|
145
|
+
.command('edit')
|
|
146
|
+
.argument('<domain>')
|
|
147
|
+
.argument('<record-id>')
|
|
148
|
+
.option('--type <type>', 'record type')
|
|
149
|
+
.option('--name <name>', 'record name, or @ for apex')
|
|
150
|
+
.option('--content <content>', 'record content')
|
|
151
|
+
.option('--ttl <seconds>', 'record TTL')
|
|
152
|
+
.option('--proxied', 'enable Cloudflare proxying')
|
|
153
|
+
.option('--no-proxied', 'disable Cloudflare proxying')
|
|
154
|
+
.option('--priority <n>', 'MX priority')
|
|
155
|
+
.option('--comment <text>', 'record comment')
|
|
156
|
+
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
157
|
+
.description('Edit a DNS record for a managed domain')
|
|
158
|
+
.action(async (domainName, recordId, opts) => {
|
|
159
|
+
try {
|
|
160
|
+
const { client, projectId } = makeClient(opts.project);
|
|
161
|
+
await runDnsEdit(client, projectId, domainName, recordId, opts);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
console.error(friendlyApiError(e));
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
dns
|
|
169
|
+
.command('delete')
|
|
170
|
+
.argument('<domain>')
|
|
171
|
+
.argument('<record-id>')
|
|
172
|
+
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
173
|
+
.option('--yes', 'skip confirmation prompt', false)
|
|
174
|
+
.description('Delete a DNS record for a managed domain')
|
|
175
|
+
.action(async (domainName, recordId, opts) => {
|
|
176
|
+
try {
|
|
177
|
+
const { client, projectId } = makeClient(opts.project);
|
|
178
|
+
await runDnsDelete(client, projectId, domainName, recordId, opts);
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
console.error(friendlyApiError(e));
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { ApiClient, friendlyApiError } from '../api.js';
|
|
4
|
+
import { requireCredentials, resolveProject } from '../config.js';
|
|
5
|
+
function makeClient(project) {
|
|
6
|
+
const creds = requireCredentials();
|
|
7
|
+
return {
|
|
8
|
+
client: new ApiClient(creds.apiUrl, creds.token),
|
|
9
|
+
projectId: resolveProject(process.cwd(), project),
|
|
10
|
+
appUrl: creds.appUrl,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function summarizeDomain(candidate) {
|
|
14
|
+
const name = String(candidate.name ?? candidate.domain ?? candidate.domain_name ?? '(unknown)');
|
|
15
|
+
const state = candidate.registrable === true || candidate.available === true
|
|
16
|
+
? 'available'
|
|
17
|
+
: candidate.registrable === false || candidate.available === false
|
|
18
|
+
? 'unavailable'
|
|
19
|
+
: 'unknown';
|
|
20
|
+
const pricing = candidate.pricing ? ` ${JSON.stringify(candidate.pricing)}` : '';
|
|
21
|
+
const reason = candidate.reason ? ` ${String(candidate.reason)}` : '';
|
|
22
|
+
return `${name} ${state}${pricing}${reason}`;
|
|
23
|
+
}
|
|
24
|
+
export function renderProjectDomain(domain) {
|
|
25
|
+
return `${domain.id} ${domain.hostname} ${domain.status} ${domain.ownership_mode}`;
|
|
26
|
+
}
|
|
27
|
+
async function confirm(prompt) {
|
|
28
|
+
const rl = createInterface({ input, output });
|
|
29
|
+
try {
|
|
30
|
+
const answer = (await rl.question(`${prompt} Type "yes" to continue: `)).trim().toLowerCase();
|
|
31
|
+
return answer === 'yes';
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
rl.close();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function runDomainSearch(client, query, limit = '10', log = console.log) {
|
|
38
|
+
const params = new URLSearchParams({ q: query, limit });
|
|
39
|
+
const result = (await client.get(`/domains/search?${params.toString()}`));
|
|
40
|
+
const candidates = result.domains ?? [];
|
|
41
|
+
log(candidates.length ? candidates.map(summarizeDomain).join('\n') : 'No suggestions.');
|
|
42
|
+
}
|
|
43
|
+
export async function runDomainCheck(client, domainNames, log = console.log) {
|
|
44
|
+
const result = (await client.post('/domains/check', { domains: domainNames }));
|
|
45
|
+
const candidates = result.domains ?? [];
|
|
46
|
+
log(candidates.length ? candidates.map(summarizeDomain).join('\n') : 'No results.');
|
|
47
|
+
}
|
|
48
|
+
export async function runDomainBuy(client, projectId, domainName, opts, log = console.log, confirmPurchase = confirm) {
|
|
49
|
+
const quoteResult = (await client.post(`/projects/${projectId}/domains/registrations/quote`, {
|
|
50
|
+
domain: domainName,
|
|
51
|
+
years: opts.years,
|
|
52
|
+
}));
|
|
53
|
+
const quote = quoteResult.quote;
|
|
54
|
+
log(JSON.stringify(quote, null, 2));
|
|
55
|
+
const ok = opts.yes ||
|
|
56
|
+
(await confirmPurchase(`Domain registrations are not refundable once complete. Buy ${domainName}?`));
|
|
57
|
+
if (!ok) {
|
|
58
|
+
log('Cancelled.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const appUrl = (opts.appUrl ?? 'https://newly.app').replace(/\/+$/, '');
|
|
62
|
+
const result = (await client.post(`/projects/${projectId}/domains/registrations/checkout`, {
|
|
63
|
+
domain: domainName,
|
|
64
|
+
years: opts.years,
|
|
65
|
+
success_url: `${appUrl}/project/${projectId}/website?domain_checkout=success`,
|
|
66
|
+
cancel_url: `${appUrl}/project/${projectId}/website?domain_checkout=cancel`,
|
|
67
|
+
}));
|
|
68
|
+
if (!result.checkout_url)
|
|
69
|
+
throw new Error('Backend did not return a checkout URL.');
|
|
70
|
+
log(`Checkout: ${result.checkout_url}`);
|
|
71
|
+
if (result.order?.id)
|
|
72
|
+
log(`Order: ${result.order.id} (${result.order.status ?? 'checkout_created'})`);
|
|
73
|
+
}
|
|
74
|
+
export async function runDomainList(client, projectId, log = console.log) {
|
|
75
|
+
const result = (await client.get(`/projects/${projectId}/domains`));
|
|
76
|
+
const projectDomains = result.domains ?? [];
|
|
77
|
+
log(projectDomains.length ? projectDomains.map(renderProjectDomain).join('\n') : 'No domains.');
|
|
78
|
+
}
|
|
79
|
+
export async function runDomainConnect(client, projectId, hostname, opts, log = console.log) {
|
|
80
|
+
const result = await client.post(`/projects/${projectId}/domains/connect`, {
|
|
81
|
+
hostname,
|
|
82
|
+
deployment_id: opts.deployment,
|
|
83
|
+
ownership_mode: opts.managed ? 'managed_cloudflare' : 'byo_dns',
|
|
84
|
+
});
|
|
85
|
+
log(renderProjectDomain(result.domain));
|
|
86
|
+
log(JSON.stringify(result.dns_instructions, null, 2));
|
|
87
|
+
}
|
|
88
|
+
export async function runDomainStatus(client, projectId, hostnameOrId, log = console.log) {
|
|
89
|
+
const list = (await client.get(`/projects/${projectId}/domains`));
|
|
90
|
+
const projectDomains = list.domains ?? [];
|
|
91
|
+
const domain = hostnameOrId
|
|
92
|
+
? projectDomains.find((item) => item.id === hostnameOrId || item.hostname === hostnameOrId)
|
|
93
|
+
: projectDomains[0];
|
|
94
|
+
if (!domain)
|
|
95
|
+
throw new Error('Domain not found.');
|
|
96
|
+
const refreshed = await client.post(`/projects/${projectId}/domains/${domain.id}/refresh`, {});
|
|
97
|
+
log(renderProjectDomain(refreshed.domain));
|
|
98
|
+
if (refreshed.dns_instructions)
|
|
99
|
+
log(JSON.stringify(refreshed.dns_instructions, null, 2));
|
|
100
|
+
}
|
|
101
|
+
export function registerDomains(program) {
|
|
102
|
+
const domains = program.command('domains').description('Search, buy, and connect domains');
|
|
103
|
+
domains
|
|
104
|
+
.command('search')
|
|
105
|
+
.argument('<query>')
|
|
106
|
+
.option('--limit <n>', 'number of suggestions', '10')
|
|
107
|
+
.description('Search available domains through Cloudflare Registrar')
|
|
108
|
+
.action(async (query, opts) => {
|
|
109
|
+
try {
|
|
110
|
+
const creds = requireCredentials();
|
|
111
|
+
const client = new ApiClient(creds.apiUrl, creds.token);
|
|
112
|
+
await runDomainSearch(client, query, opts.limit);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
console.error(friendlyApiError(e));
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
domains
|
|
120
|
+
.command('check')
|
|
121
|
+
.argument('<domains...>')
|
|
122
|
+
.description('Check domain availability through Cloudflare Registrar')
|
|
123
|
+
.action(async (domainNames) => {
|
|
124
|
+
try {
|
|
125
|
+
const creds = requireCredentials();
|
|
126
|
+
const client = new ApiClient(creds.apiUrl, creds.token);
|
|
127
|
+
await runDomainCheck(client, domainNames);
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.error(friendlyApiError(e));
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
domains
|
|
135
|
+
.command('buy')
|
|
136
|
+
.argument('<domain>')
|
|
137
|
+
.option('--years <n>', 'registration term in years', '1')
|
|
138
|
+
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
139
|
+
.option('--yes', 'confirm purchase without an interactive prompt', false)
|
|
140
|
+
.description('Buy a domain through Cloudflare Registrar')
|
|
141
|
+
.action(async (domainName, opts) => {
|
|
142
|
+
try {
|
|
143
|
+
const { client, projectId, appUrl } = makeClient(opts.project);
|
|
144
|
+
await runDomainBuy(client, projectId, domainName, {
|
|
145
|
+
years: Number(opts.years),
|
|
146
|
+
yes: opts.yes,
|
|
147
|
+
appUrl,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
console.error(friendlyApiError(e));
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
domains
|
|
156
|
+
.command('list')
|
|
157
|
+
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
158
|
+
.description('List domains connected to a project')
|
|
159
|
+
.action(async (opts) => {
|
|
160
|
+
try {
|
|
161
|
+
const { client, projectId } = makeClient(opts.project);
|
|
162
|
+
await runDomainList(client, projectId);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
console.error(friendlyApiError(e));
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
domains
|
|
170
|
+
.command('connect')
|
|
171
|
+
.argument('<hostname>')
|
|
172
|
+
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
173
|
+
.option('--deployment <id>', 'deployment id to serve')
|
|
174
|
+
.option('--managed', 'domain DNS is managed by Newly/Cloudflare', false)
|
|
175
|
+
.description('Connect an existing domain to the published website')
|
|
176
|
+
.action(async (hostname, opts) => {
|
|
177
|
+
try {
|
|
178
|
+
const { client, projectId } = makeClient(opts.project);
|
|
179
|
+
await runDomainConnect(client, projectId, hostname, opts);
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
console.error(friendlyApiError(e));
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
domains
|
|
187
|
+
.command('status')
|
|
188
|
+
.argument('[hostname-or-id]')
|
|
189
|
+
.option('--project <id>', 'project id (defaults to the linked directory)')
|
|
190
|
+
.description('Show or refresh connected domain status')
|
|
191
|
+
.action(async (hostnameOrId, opts) => {
|
|
192
|
+
try {
|
|
193
|
+
const { client, projectId } = makeClient(opts.project);
|
|
194
|
+
await runDomainStatus(client, projectId, hostnameOrId);
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
console.error(friendlyApiError(e));
|
|
198
|
+
process.exitCode = 1;
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ApiClient, friendlyApiError } from '../api.js';
|
|
2
|
+
import { requireCredentials, resolveProject } from '../config.js';
|
|
3
|
+
import { assertDevOnly } from './misc.js';
|
|
4
|
+
function makeClient(project) {
|
|
5
|
+
const creds = requireCredentials();
|
|
6
|
+
return {
|
|
7
|
+
client: new ApiClient(creds.apiUrl, creds.token),
|
|
8
|
+
projectId: resolveProject(process.cwd(), project),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export async function getEmailStatus(client, projectId, env) {
|
|
12
|
+
return await client.get(`/newly_backend/email/status?project_id=${encodeURIComponent(projectId)}&env=${env}`);
|
|
13
|
+
}
|
|
14
|
+
export function formatEmailStatus(status) {
|
|
15
|
+
const lines = [
|
|
16
|
+
`email: ${status.configured ? 'configured' : 'not configured'}`,
|
|
17
|
+
`from: ${status.from ?? '(not set)'}`,
|
|
18
|
+
];
|
|
19
|
+
if (status.requires_deploy)
|
|
20
|
+
lines.push('requires deploy: yes');
|
|
21
|
+
if (status.notes)
|
|
22
|
+
lines.push(`notes: ${status.notes}`);
|
|
23
|
+
return lines.join('\n');
|
|
24
|
+
}
|
|
25
|
+
export async function sendTestEmail(client, opts) {
|
|
26
|
+
return await client.post('/newly_backend/email/test', {
|
|
27
|
+
project_id: opts.projectId,
|
|
28
|
+
env: opts.env,
|
|
29
|
+
to: opts.to,
|
|
30
|
+
from: opts.from,
|
|
31
|
+
subject: opts.subject,
|
|
32
|
+
text: opts.text,
|
|
33
|
+
html: opts.html,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function registerEmail(program) {
|
|
37
|
+
const email = program.command('email').description('Manage Newly Email');
|
|
38
|
+
email
|
|
39
|
+
.command('status')
|
|
40
|
+
.description('Show Newly Email configuration for this backend')
|
|
41
|
+
.option('--env <env>', 'dev or prod', 'dev')
|
|
42
|
+
.option('--project <id>', 'project id')
|
|
43
|
+
.action(async (opts) => {
|
|
44
|
+
try {
|
|
45
|
+
assertDevOnly(opts.env, 'email status');
|
|
46
|
+
const { client, projectId } = makeClient(opts.project);
|
|
47
|
+
console.log(formatEmailStatus(await getEmailStatus(client, projectId, opts.env)));
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
console.error(friendlyApiError(e));
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
email
|
|
55
|
+
.command('test')
|
|
56
|
+
.description('Send a test email through the deployed API service')
|
|
57
|
+
.requiredOption('--to <email>', 'recipient email address')
|
|
58
|
+
.option('--from <email>', 'sender email address; defaults to the backend sender setting')
|
|
59
|
+
.option('--subject <subject>', 'email subject', 'Newly email test')
|
|
60
|
+
.option('--text <text>', 'plain-text body', 'This is a test email from your Newly backend.')
|
|
61
|
+
.option('--html <html>', 'optional HTML body')
|
|
62
|
+
.option('--env <env>', 'dev or prod', 'dev')
|
|
63
|
+
.option('--project <id>', 'project id')
|
|
64
|
+
.action(async (opts) => {
|
|
65
|
+
try {
|
|
66
|
+
assertDevOnly(opts.env, 'email test');
|
|
67
|
+
const { client, projectId } = makeClient(opts.project);
|
|
68
|
+
const result = await sendTestEmail(client, {
|
|
69
|
+
projectId,
|
|
70
|
+
env: opts.env,
|
|
71
|
+
to: opts.to,
|
|
72
|
+
from: opts.from,
|
|
73
|
+
subject: opts.subject,
|
|
74
|
+
text: opts.text,
|
|
75
|
+
html: opts.html,
|
|
76
|
+
});
|
|
77
|
+
console.log(result.message_id
|
|
78
|
+
? `Email sent. message_id=${result.message_id}`
|
|
79
|
+
: 'Email sent.');
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
console.error(friendlyApiError(e));
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import { clearCredentials, saveCredentials } from '../config.js';
|
|
5
|
-
const DEFAULT_APP_URL = 'https://newly.app';
|
|
4
|
+
import { clearCredentials, credentialsDisplayPath, saveCredentials } from '../config.js';
|
|
5
|
+
const DEFAULT_APP_URL = process.env.NEWLY_APP_URL ?? 'https://newly.app';
|
|
6
6
|
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
7
7
|
export async function startCallbackServer(state, appOrigin, timeoutMs) {
|
|
8
8
|
let resolvePayload;
|
|
@@ -96,8 +96,32 @@ export function registerLogin(program) {
|
|
|
96
96
|
.description('Log in with your Newly account via the browser')
|
|
97
97
|
.option('--app-url <url>', 'Newly web app URL', DEFAULT_APP_URL)
|
|
98
98
|
.option('--manual', 'paste the credential from the browser instead of using the local callback', false)
|
|
99
|
+
.option('--api-url <url>', 'explicit API URL (skips browser; use with --token)')
|
|
100
|
+
.option('--token <token>', 'explicit credential token (skips browser; use with --api-url)')
|
|
101
|
+
.option('--expires-in <seconds>', 'session lifetime when using --token (default 7 days)', (v) => Number(v), 604800)
|
|
99
102
|
.action(async (opts) => {
|
|
100
103
|
try {
|
|
104
|
+
const hasApiUrl = typeof opts.apiUrl === 'string' && opts.apiUrl.length > 0;
|
|
105
|
+
const hasToken = typeof opts.token === 'string' && opts.token.length > 0;
|
|
106
|
+
if (hasApiUrl !== hasToken) {
|
|
107
|
+
throw new Error('--api-url and --token must be provided together for headless login.');
|
|
108
|
+
}
|
|
109
|
+
if (hasApiUrl && hasToken) {
|
|
110
|
+
try {
|
|
111
|
+
new URL(opts.apiUrl);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new Error(`--api-url is not a valid URL: ${opts.apiUrl}`);
|
|
115
|
+
}
|
|
116
|
+
saveCredentials({
|
|
117
|
+
token: opts.token,
|
|
118
|
+
apiUrl: opts.apiUrl,
|
|
119
|
+
appUrl: opts.appUrl,
|
|
120
|
+
expiresAt: Math.floor(Date.now() / 1000) + opts.expiresIn,
|
|
121
|
+
});
|
|
122
|
+
console.log(`Logged in headlessly. Credentials written for API ${opts.apiUrl}.`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
101
125
|
if (opts.manual) {
|
|
102
126
|
const { createInterface } = await import('node:readline/promises');
|
|
103
127
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -128,7 +152,7 @@ export function registerLogin(program) {
|
|
|
128
152
|
appUrl: opts.appUrl,
|
|
129
153
|
expiresAt: Math.floor(Date.now() / 1000) + payload.expiresIn,
|
|
130
154
|
});
|
|
131
|
-
console.log(
|
|
155
|
+
console.log(`Logged in. Credentials saved to ${credentialsDisplayPath()}`);
|
|
132
156
|
}
|
|
133
157
|
finally {
|
|
134
158
|
close();
|
package/dist/commands/misc.js
CHANGED
|
@@ -30,13 +30,34 @@ export function resolveLogWindow(opts, now = Date.now()) {
|
|
|
30
30
|
validateIso(opts.end);
|
|
31
31
|
return { start, end: opts.end };
|
|
32
32
|
}
|
|
33
|
-
export
|
|
34
|
-
|
|
33
|
+
export function formatLogEntry(e) {
|
|
34
|
+
if (!e || typeof e !== 'object')
|
|
35
|
+
return String(e);
|
|
36
|
+
const ts = e.ts ?? '-';
|
|
37
|
+
const sev = (e.severity ?? 'OK').padEnd(5);
|
|
38
|
+
const trace = e.trace_id ? ` trace=${e.trace_id.slice(0, 16)}` : '';
|
|
39
|
+
const name = e.name ?? '';
|
|
40
|
+
return `${ts} ${sev} ${name}${trace}`.trimEnd();
|
|
41
|
+
}
|
|
42
|
+
export async function fetchLogs(client, projectId, env, service, pageSize, bounds = {}, filters = {}) {
|
|
43
|
+
const params = new URLSearchParams();
|
|
44
|
+
params.set('env', env);
|
|
45
|
+
params.set('service', service);
|
|
46
|
+
params.set('page_size', String(pageSize));
|
|
35
47
|
if (bounds.start)
|
|
36
|
-
|
|
48
|
+
params.set('start', bounds.start);
|
|
37
49
|
if (bounds.end)
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
params.set('end', bounds.end);
|
|
51
|
+
if (filters.severityMin)
|
|
52
|
+
params.set('severity_min', filters.severityMin);
|
|
53
|
+
if (filters.traceId)
|
|
54
|
+
params.set('trace_id', filters.traceId);
|
|
55
|
+
if (filters.textSearch)
|
|
56
|
+
params.set('text_search', filters.textSearch);
|
|
57
|
+
const out = await client.get(`/newly_backend/logs/${encodeURIComponent(projectId)}?${params.toString()}`);
|
|
58
|
+
if (Array.isArray(out.entries)) {
|
|
59
|
+
return out.entries.map((e) => formatLogEntry(e)).join('\n');
|
|
60
|
+
}
|
|
40
61
|
if (typeof out.logs === 'string')
|
|
41
62
|
return out.logs;
|
|
42
63
|
if (Array.isArray(out.lines))
|
|
@@ -75,18 +96,24 @@ export function registerMisc(program) {
|
|
|
75
96
|
.command('logs')
|
|
76
97
|
.description('Tail backend service logs')
|
|
77
98
|
.option('--env <env>', 'dev or prod', 'dev')
|
|
78
|
-
.option('--service <service>', 'api
|
|
79
|
-
.option('-n, --lines <n>', 'number of
|
|
99
|
+
.option('--service <service>', 'api | auth', 'api')
|
|
100
|
+
.option('-n, --lines <n>', 'page size (number of log entries to fetch)', '200')
|
|
80
101
|
.option('--since <duration>', 'relative window, e.g. 5m, 1h, 30s')
|
|
81
102
|
.option('--start <iso>', 'window start (ISO 8601 timestamp)')
|
|
82
103
|
.option('--end <iso>', 'window end (ISO 8601 timestamp)')
|
|
104
|
+
.option('--severity <min>', 'min severity: ok | warn | error')
|
|
105
|
+
.option('--trace <id>', 'restrict to a single trace_id')
|
|
106
|
+
.option('--search <text>', 'substring filter on the entry name')
|
|
83
107
|
.option('--project <id>', 'project id')
|
|
84
108
|
.action(async (opts) => {
|
|
85
109
|
try {
|
|
86
110
|
assertDevOnly(opts.env, 'logs');
|
|
111
|
+
if (opts.service !== 'api' && opts.service !== 'auth') {
|
|
112
|
+
throw new Error(`--service must be 'api' or 'auth' (got ${JSON.stringify(opts.service)})`);
|
|
113
|
+
}
|
|
87
114
|
const bounds = resolveLogWindow(opts);
|
|
88
115
|
const { client, projectId } = makeClient(opts.project);
|
|
89
|
-
console.log(await fetchLogs(client, projectId, opts.env, opts.service, Number(opts.lines), bounds));
|
|
116
|
+
console.log(await fetchLogs(client, projectId, opts.env, opts.service, Number(opts.lines), bounds, { severityMin: opts.severity, traceId: opts.trace, textSearch: opts.search }));
|
|
90
117
|
}
|
|
91
118
|
catch (e) {
|
|
92
119
|
console.error(friendlyApiError(e));
|
|
@@ -97,7 +124,7 @@ export function registerMisc(program) {
|
|
|
97
124
|
secrets
|
|
98
125
|
.command('list')
|
|
99
126
|
.option('--env <env>', 'dev or prod', 'dev')
|
|
100
|
-
.option('--service <service>', '
|
|
127
|
+
.option('--service <service>', 'backend service name', 'worker')
|
|
101
128
|
.option('--project <id>', 'project id')
|
|
102
129
|
.action(async (opts) => {
|
|
103
130
|
try {
|
|
@@ -115,7 +142,7 @@ export function registerMisc(program) {
|
|
|
115
142
|
.argument('<key>')
|
|
116
143
|
.argument('<value>')
|
|
117
144
|
.option('--env <env>', 'dev or prod', 'dev')
|
|
118
|
-
.option('--service <service>', '
|
|
145
|
+
.option('--service <service>', 'backend service name', 'worker')
|
|
119
146
|
.option('--project <id>', 'project id')
|
|
120
147
|
.action(async (key, value, opts) => {
|
|
121
148
|
try {
|
|
@@ -131,7 +158,7 @@ export function registerMisc(program) {
|
|
|
131
158
|
});
|
|
132
159
|
const db = program.command('db').description('Database helpers');
|
|
133
160
|
db.command('url')
|
|
134
|
-
.description('Print a readonly
|
|
161
|
+
.description('Print a readonly URL for the Newly DB')
|
|
135
162
|
.option('--env <env>', 'dev or prod', 'dev')
|
|
136
163
|
.option('--project <id>', 'project id')
|
|
137
164
|
.action(async (opts) => {
|