runcloud 0.2.177 → 0.2.179
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/dist/commands/domains.js +13 -4
- package/dist/commands/email.js +261 -11
- package/dist/commands/run-cloud-fleet-releases.js +26 -0
- package/dist/commands/run-cloud-fleet.js +15 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/runcloud-0.2.179.tgz +0 -0
- package/runcloud-0.2.177.tgz +0 -0
package/dist/commands/domains.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { stdin as input, stdout as output } from 'node:process';
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
|
-
import { ApiClient, friendlyApiError } from '../api.js';
|
|
3
|
+
import { ApiClient, ApiError, friendlyApiError } from '../api.js';
|
|
4
4
|
import { requireCredentials, resolveProject } from '../config.js';
|
|
5
5
|
export function renderPendingRegistration(order) {
|
|
6
6
|
const action = `run \`newly domains connect-registered ${order.domain}\``;
|
|
@@ -27,9 +27,9 @@ export function summarizeDomain(candidate) {
|
|
|
27
27
|
return `${name} ${state}${pricing}${reason}`;
|
|
28
28
|
}
|
|
29
29
|
export function renderProjectDomain(domain) {
|
|
30
|
-
return `${domain.id} ${domain.hostname} ${domain.status} ${domain.ownership_mode}`;
|
|
30
|
+
return `${domain.id} ${domain.hostname} ${domain.status} ${domain.ownership_mode} email: ${domain.email_status ?? 'not set up'}`;
|
|
31
31
|
}
|
|
32
|
-
async function confirm(prompt) {
|
|
32
|
+
export async function confirm(prompt) {
|
|
33
33
|
const rl = createInterface({ input, output });
|
|
34
34
|
try {
|
|
35
35
|
const answer = (await rl.question(`${prompt} Type "yes" to continue: `)).trim().toLowerCase();
|
|
@@ -116,7 +116,16 @@ export async function runDomainStatus(client, projectId, hostnameOrId, log = con
|
|
|
116
116
|
}
|
|
117
117
|
if (!domain)
|
|
118
118
|
throw new Error('Domain not found.');
|
|
119
|
-
|
|
119
|
+
let refreshed;
|
|
120
|
+
try {
|
|
121
|
+
refreshed = await client.post(`/projects/${projectId}/domains/${domain.id}/refresh`, {});
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (!(e instanceof ApiError && e.status === 403))
|
|
125
|
+
throw e;
|
|
126
|
+
log(renderProjectDomain(domain));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
120
129
|
log(renderProjectDomain(refreshed.domain));
|
|
121
130
|
if (refreshed.dns_instructions)
|
|
122
131
|
log(JSON.stringify(refreshed.dns_instructions, null, 2));
|
package/dist/commands/email.js
CHANGED
|
@@ -1,13 +1,263 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { readFileSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { ApiClient, ApiError, friendlyApiError } from '../api.js';
|
|
5
|
+
import { requireCredentials, resolveProject } from '../config.js';
|
|
6
|
+
import { confirm } from './domains.js';
|
|
7
|
+
export const ASK_USER_TO_SEND = 'Ask the user to review and send it in Newly';
|
|
8
|
+
export const NEEDS_TERMINAL = 'Sending needs you at a terminal. Review and send it in Newly instead.';
|
|
9
|
+
const stdinIsTty = () => Boolean(process.stdin.isTTY);
|
|
10
|
+
const base = (projectId) => `/projects/${projectId}/email`;
|
|
11
|
+
const show = (log, value) => log(JSON.stringify(value, null, 2));
|
|
12
|
+
const campaignLine = (c) => `${c.id} ${c.status} ${c.name}`;
|
|
13
|
+
export async function runEmailSettings(client, projectId, opts, log = console.log) {
|
|
14
|
+
const body = {
|
|
15
|
+
...(opts.senderName !== undefined && { sender_name: opts.senderName }),
|
|
16
|
+
...(opts.postalAddress !== undefined && { postal_address: opts.postalAddress }),
|
|
17
|
+
...(opts.replyTo !== undefined && { reply_to: opts.replyTo }),
|
|
18
|
+
};
|
|
19
|
+
show(log, Object.keys(body).length
|
|
20
|
+
? await client.put(`${base(projectId)}/settings`, body)
|
|
21
|
+
: await client.get(`${base(projectId)}/settings`));
|
|
22
|
+
}
|
|
23
|
+
export async function runContactsCount(client, projectId, log = console.log) {
|
|
24
|
+
show(log, await client.get(`${base(projectId)}/contacts/counts`));
|
|
25
|
+
}
|
|
26
|
+
export async function runContactsList(client, projectId, opts, log = console.log) {
|
|
27
|
+
const params = new URLSearchParams();
|
|
28
|
+
if (opts.status)
|
|
29
|
+
params.set('status', opts.status);
|
|
30
|
+
if (opts.q)
|
|
31
|
+
params.set('q', opts.q);
|
|
32
|
+
const query = params.toString();
|
|
33
|
+
show(log, await client.get(`${base(projectId)}/contacts${query ? `?${query}` : ''}`));
|
|
34
|
+
}
|
|
35
|
+
export async function runCampaignList(client, projectId, log = console.log) {
|
|
36
|
+
const { campaigns = [] } = (await client.get(`${base(projectId)}/campaigns`));
|
|
37
|
+
log(campaigns.length ? campaigns.map(campaignLine).join('\n') : 'No campaigns.');
|
|
38
|
+
}
|
|
39
|
+
function readHtml(path) {
|
|
40
|
+
try {
|
|
41
|
+
return readFileSync(path, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error(`Could not read ${path}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function runCampaignDraft(client, projectId, opts, log = console.log) {
|
|
48
|
+
const body = {
|
|
49
|
+
...(opts.name !== undefined && { name: opts.name }),
|
|
50
|
+
...(opts.subject !== undefined && { subject: opts.subject }),
|
|
51
|
+
...(opts.previewText !== undefined && { preview_text: opts.previewText }),
|
|
52
|
+
...(opts.topic !== undefined && { topic_key: opts.topic }),
|
|
53
|
+
...(opts.htmlFile !== undefined && { html: readHtml(opts.htmlFile) }),
|
|
54
|
+
};
|
|
55
|
+
if (!opts.id && !opts.name)
|
|
56
|
+
throw new Error('A new campaign needs --name.');
|
|
57
|
+
const saved = (opts.id
|
|
58
|
+
? await client.patch(`${base(projectId)}/campaigns/${opts.id}`, body)
|
|
59
|
+
: await client.post(`${base(projectId)}/campaigns`, body));
|
|
60
|
+
log(campaignLine(saved));
|
|
61
|
+
}
|
|
62
|
+
export async function runCampaignPreview(client, projectId, id, log = console.log) {
|
|
63
|
+
const campaign = (await client.get(`${base(projectId)}/campaigns/${id}`));
|
|
64
|
+
const file = join(mkdtempSync(join(tmpdir(), 'newly-campaign-')), 'preview.html');
|
|
65
|
+
writeFileSync(file, campaign.html);
|
|
66
|
+
log(file);
|
|
67
|
+
}
|
|
68
|
+
export async function runCampaignTest(client, projectId, id, log = console.log) {
|
|
69
|
+
await client.post(`${base(projectId)}/campaigns/${id}/test`, {});
|
|
70
|
+
log('Test email sent to your account address.');
|
|
71
|
+
}
|
|
72
|
+
export async function runCampaignStatus(client, projectId, id, log = console.log) {
|
|
73
|
+
const campaign = (await client.get(`${base(projectId)}/campaigns/${id}`));
|
|
74
|
+
const estimate = (await client.get(`${base(projectId)}/campaigns/${id}/estimate`));
|
|
75
|
+
log(campaignLine(campaign));
|
|
76
|
+
show(log, { estimate });
|
|
77
|
+
}
|
|
78
|
+
async function confirmedPost(client, projectId, id, action, extra, verb, log, confirmFn, isTty) {
|
|
79
|
+
if (!isTty())
|
|
80
|
+
throw new Error(NEEDS_TERMINAL);
|
|
81
|
+
const campaign = (await client.get(`${base(projectId)}/campaigns/${id}`));
|
|
82
|
+
const estimate = (await client.get(`${base(projectId)}/campaigns/${id}/estimate`));
|
|
83
|
+
log(`${campaign.name}: ${estimate.recipients} recipients, ${estimate.credits} credits.`);
|
|
84
|
+
if (!(await confirmFn(`${verb} "${campaign.name}"?`))) {
|
|
85
|
+
log('Cancelled.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const result = (await client.post(`${base(projectId)}/campaigns/${id}/${action}`, {
|
|
89
|
+
...extra,
|
|
90
|
+
expected_updated_at: campaign.updated_at,
|
|
91
|
+
}));
|
|
92
|
+
log(campaignLine(result));
|
|
93
|
+
}
|
|
94
|
+
export const runCampaignSend = (client, projectId, id, log = console.log, confirmFn = confirm, isTty = stdinIsTty) => confirmedPost(client, projectId, id, 'send', {}, 'Send', log, confirmFn, isTty);
|
|
95
|
+
export const runCampaignSchedule = (client, projectId, id, opts, log = console.log, confirmFn = confirm, isTty = stdinIsTty) => confirmedPost(client, projectId, id, 'schedule', { at: opts.at, timezone: opts.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone }, `Schedule for ${opts.at}`, log, confirmFn, isTty);
|
|
96
|
+
export const USER_SESSION_ONLY = 'This needs a signed-in user session. Ask the user to do it in Newly.';
|
|
97
|
+
const PENDING_NOTE = 'Newly checks every minute; this usually takes 5 to 30 minutes after the records are added.';
|
|
98
|
+
const relativeName = (name, root) => name === root ? '@' : name.endsWith(`.${root}`) ? name.slice(0, -root.length - 1) : name;
|
|
99
|
+
function recordLine(r, root) {
|
|
100
|
+
const content = r.type === 'TXT' ? r.content.replace(/^"(.*)"$/, '$1') : r.content;
|
|
101
|
+
const value = r.priority === undefined ? content : `${r.priority} ${content}`;
|
|
102
|
+
const note = r.optional ? ' (optional, you already have one)' : '';
|
|
103
|
+
return ` ${r.type} ${r.name} (relative: ${relativeName(r.name, root)}) ${value}${note}`;
|
|
104
|
+
}
|
|
105
|
+
function domainLines(d) {
|
|
106
|
+
const status = d.status ?? 'pending';
|
|
107
|
+
const head = `${d.hostname} ${d.kind} ${status}`;
|
|
108
|
+
if (d.kind !== 'own' || status === 'verified')
|
|
109
|
+
return [head];
|
|
110
|
+
return [
|
|
111
|
+
head,
|
|
112
|
+
...(d.reason ? [` ${d.reason.replace(/ Press Check now.*/i, '')}`] : []),
|
|
113
|
+
...(d.records ?? []).map((r) => recordLine(r, d.root_domain)),
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
const RECORDS_POLL_MS = 3_000;
|
|
117
|
+
const RECORDS_POLL_TRIES = 10;
|
|
118
|
+
const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
119
|
+
async function waitForRecords(client, projectId, claim, sleep) {
|
|
120
|
+
let d = claim;
|
|
121
|
+
for (let i = 0; i < RECORDS_POLL_TRIES && !d.records && d.status !== 'verified' && d.status !== 'failed'; i++) {
|
|
122
|
+
await sleep(RECORDS_POLL_MS);
|
|
123
|
+
const { domains = [] } = (await client.get(`${base(projectId)}/domains`));
|
|
124
|
+
d = domains.find((x) => x.id === claim.id) ?? d;
|
|
125
|
+
}
|
|
126
|
+
return d;
|
|
127
|
+
}
|
|
128
|
+
export async function runEmailDomainAdd(client, projectId, hostname, log = console.log, sleep = realSleep) {
|
|
129
|
+
const claim = (await client.post(`${base(projectId)}/domains`, { hostname }));
|
|
130
|
+
if (claim.kind === 'managed') {
|
|
131
|
+
log(`Newly manages DNS for ${claim.hostname}, so email is set up automatically. There is nothing to add.`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (claim.status === 'verified') {
|
|
135
|
+
log(`${claim.hostname} is verified. Email sends from it.`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!claim.records)
|
|
139
|
+
log(`Preparing the DNS records for ${claim.hostname}...`);
|
|
140
|
+
const d = await waitForRecords(client, projectId, claim, sleep);
|
|
141
|
+
if (d.status === 'failed' && d.reason)
|
|
142
|
+
log(d.reason.replace(/ Press Check now.*/i, ''));
|
|
143
|
+
if (!d.records) {
|
|
144
|
+
log('The records are not ready yet. Run "newly email domain status" in a minute to see them.');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
log(`Add these DNS records at your DNS provider for ${d.hostname}:`);
|
|
148
|
+
log(d.records.map((r) => recordLine(r, d.root_domain)).join('\n'));
|
|
149
|
+
log(`Status: ${d.status ?? 'pending'}. ${PENDING_NOTE}`);
|
|
150
|
+
}
|
|
151
|
+
export async function runEmailDomainStatus(client, projectId, log = console.log) {
|
|
152
|
+
const { domains = [] } = (await client.get(`${base(projectId)}/domains`));
|
|
153
|
+
log(domains.length ? domains.flatMap(domainLines).join('\n') : 'No email domains.');
|
|
154
|
+
}
|
|
155
|
+
export function emailDomainErrorMessage(e) {
|
|
156
|
+
return e instanceof ApiError && e.status === 403 ? USER_SESSION_ONLY : friendlyApiError(e);
|
|
157
|
+
}
|
|
158
|
+
export function emailErrorMessage(e, isSend = false) {
|
|
159
|
+
return isSend && e instanceof ApiError && e.status === 403 ? ASK_USER_TO_SEND : friendlyApiError(e);
|
|
160
|
+
}
|
|
3
161
|
export function registerEmail(program) {
|
|
4
|
-
program
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
162
|
+
const email = program.command('email').description('Marketing email: settings, contacts and campaigns');
|
|
163
|
+
const run = (fn, isSend = false) => async (opts) => {
|
|
164
|
+
try {
|
|
165
|
+
const creds = requireCredentials();
|
|
166
|
+
await fn(new ApiClient(creds.apiUrl, creds.token), resolveProject(process.cwd(), opts.project));
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
console.error(emailErrorMessage(e, isSend));
|
|
170
|
+
process.exitCode = 1;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const project = '--project <id>';
|
|
174
|
+
const projectHelp = 'project id (defaults to the linked directory)';
|
|
175
|
+
email
|
|
176
|
+
.command('settings')
|
|
177
|
+
.option(project, projectHelp)
|
|
178
|
+
.option('--sender-name <name>')
|
|
179
|
+
.option('--postal-address <address>')
|
|
180
|
+
.option('--reply-to <email>')
|
|
181
|
+
.description('Show email settings, or update them when a flag is given')
|
|
182
|
+
.action((opts) => run((c, p) => runEmailSettings(c, p, opts))(opts));
|
|
183
|
+
const contacts = email.command('contacts').description('Marketing contacts');
|
|
184
|
+
contacts
|
|
185
|
+
.command('count')
|
|
186
|
+
.option(project, projectHelp)
|
|
187
|
+
.action((opts) => run((c, p) => runContactsCount(c, p))(opts));
|
|
188
|
+
contacts
|
|
189
|
+
.command('list')
|
|
190
|
+
.option(project, projectHelp)
|
|
191
|
+
.option('--status <status>')
|
|
192
|
+
.option('--q <text>', 'search by email')
|
|
193
|
+
.action((opts) => run((c, p) => runContactsList(c, p, opts))(opts));
|
|
194
|
+
const campaign = email.command('campaign').description('Draft, preview, test and send campaigns');
|
|
195
|
+
campaign
|
|
196
|
+
.command('list')
|
|
197
|
+
.option(project, projectHelp)
|
|
198
|
+
.action((opts) => run((c, p) => runCampaignList(c, p))(opts));
|
|
199
|
+
campaign
|
|
200
|
+
.command('draft')
|
|
201
|
+
.option(project, projectHelp)
|
|
202
|
+
.option('--id <id>', 'edit this draft instead of creating one')
|
|
203
|
+
.option('--name <name>')
|
|
204
|
+
.option('--subject <subject>')
|
|
205
|
+
.option('--preview-text <text>')
|
|
206
|
+
.option('--topic <key>')
|
|
207
|
+
.option('--html-file <path>')
|
|
208
|
+
.description('Create a draft campaign, or update one with --id')
|
|
209
|
+
.action((opts) => run((c, p) => runCampaignDraft(c, p, opts))(opts));
|
|
210
|
+
campaign
|
|
211
|
+
.command('preview')
|
|
212
|
+
.argument('<id>')
|
|
213
|
+
.option(project, projectHelp)
|
|
214
|
+
.description('Write the campaign HTML to a temp file and print its path')
|
|
215
|
+
.action((id, opts) => run((c, p) => runCampaignPreview(c, p, id))(opts));
|
|
216
|
+
campaign
|
|
217
|
+
.command('test')
|
|
218
|
+
.argument('<id>')
|
|
219
|
+
.option(project, projectHelp)
|
|
220
|
+
.description('Send a test email to your own account address')
|
|
221
|
+
.action((id, opts) => run((c, p) => runCampaignTest(c, p, id))(opts));
|
|
222
|
+
campaign
|
|
223
|
+
.command('status')
|
|
224
|
+
.argument('<id>')
|
|
225
|
+
.option(project, projectHelp)
|
|
226
|
+
.action((id, opts) => run((c, p) => runCampaignStatus(c, p, id))(opts));
|
|
227
|
+
campaign
|
|
228
|
+
.command('send')
|
|
229
|
+
.argument('<id>')
|
|
230
|
+
.option(project, projectHelp)
|
|
231
|
+
.description('Send a campaign now (needs a terminal and confirmation)')
|
|
232
|
+
.action((id, opts) => run((c, p) => runCampaignSend(c, p, id), true)(opts));
|
|
233
|
+
campaign
|
|
234
|
+
.command('schedule')
|
|
235
|
+
.argument('<id>')
|
|
236
|
+
.requiredOption('--at <iso>', 'ISO date and time')
|
|
237
|
+
.option('--timezone <iana>', 'IANA timezone (defaults to this machine)')
|
|
238
|
+
.option(project, projectHelp)
|
|
239
|
+
.description('Schedule a campaign (needs a terminal and confirmation)')
|
|
240
|
+
.action((id, opts) => run((c, p) => runCampaignSchedule(c, p, id, opts), true)(opts));
|
|
241
|
+
const domain = email.command('domain').description('Send email from a domain you own');
|
|
242
|
+
const runDomain = (fn) => async (opts) => {
|
|
243
|
+
try {
|
|
244
|
+
const creds = requireCredentials();
|
|
245
|
+
await fn(new ApiClient(creds.apiUrl, creds.token), resolveProject(process.cwd(), opts.project));
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
console.error(emailDomainErrorMessage(e));
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
domain
|
|
253
|
+
.command('add')
|
|
254
|
+
.argument('<hostname>')
|
|
255
|
+
.option(project, projectHelp)
|
|
256
|
+
.description('Claim a domain and print the DNS records to add')
|
|
257
|
+
.action((hostname, opts) => runDomain((c, p) => runEmailDomainAdd(c, p, hostname))(opts));
|
|
258
|
+
domain
|
|
259
|
+
.command('status')
|
|
260
|
+
.option(project, projectHelp)
|
|
261
|
+
.description('List email domains and the records still to add')
|
|
262
|
+
.action((opts) => runDomain((c, p) => runEmailDomainStatus(c, p))(opts));
|
|
13
263
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const PATH = '/run-cloud/admin/fleet-releases';
|
|
2
|
+
export const RELEASE_CHANNELS = ['ios/dev', 'ios/prod', 'android/dev', 'android/prod'];
|
|
3
|
+
export async function listReleaseChannels(client) {
|
|
4
|
+
const body = await client.get(PATH, { signal: AbortSignal.timeout(30000) });
|
|
5
|
+
if (!body?.configured)
|
|
6
|
+
throw new Error('Fleet release storage is not configured on this API');
|
|
7
|
+
return { unavailable: body.unavailable === true, channels: body.channels ?? [] };
|
|
8
|
+
}
|
|
9
|
+
export async function promoteReleaseChannel(client, input) {
|
|
10
|
+
if (!RELEASE_CHANNELS.includes(input.channel))
|
|
11
|
+
throw new Error(`Channel must be one of ${RELEASE_CHANNELS.join(', ')}`);
|
|
12
|
+
const { channels, unavailable } = await listReleaseChannels(client);
|
|
13
|
+
if (unavailable)
|
|
14
|
+
throw new Error('Fleet release storage is unavailable; nothing was promoted');
|
|
15
|
+
const current = channels.find(channel => channel.channel === input.channel);
|
|
16
|
+
return await client.post(PATH, { channel: input.channel, version: input.version, reason: input.reason,
|
|
17
|
+
digest: input.digest ?? null, artifact: input.artifact ?? null, expectedRevision: current?.revision ?? 0 }, { signal: AbortSignal.timeout(30000) });
|
|
18
|
+
}
|
|
19
|
+
export function releaseText(channels) {
|
|
20
|
+
return RELEASE_CHANNELS.map(name => {
|
|
21
|
+
const channel = channels.find(item => item.channel === name);
|
|
22
|
+
return channel
|
|
23
|
+
? `${name.padEnd(13)} ${channel.version} r${channel.revision} by ${channel.promotedBy} at ${channel.promotedAt}${channel.digest ? ` ${channel.digest}` : ''}`
|
|
24
|
+
: `${name.padEnd(13)} unset (hosts use their inventory pin)`;
|
|
25
|
+
}).join('\n');
|
|
26
|
+
}
|
|
@@ -4,6 +4,7 @@ import { readFileSync, statSync } from 'node:fs';
|
|
|
4
4
|
import { ApiClient, ApiError } from '../api.js';
|
|
5
5
|
import { requireCredentials } from '../config.js';
|
|
6
6
|
import { surveyFleet, surveyText } from './run-cloud-fleet-survey.js';
|
|
7
|
+
import { listReleaseChannels, promoteReleaseChannel, releaseText } from './run-cloud-fleet-releases.js';
|
|
7
8
|
const ROOT = '/run-cloud/admin/simulators';
|
|
8
9
|
const RANGES = ['1h', '6h', '24h', '7d', '30d', '90d', '1y', '2y'];
|
|
9
10
|
export const ACCESSIBILITY_CODES = ['accessibility_device_unavailable', 'accessibility_capture_failed'];
|
|
@@ -300,7 +301,7 @@ function printReport(report, json) {
|
|
|
300
301
|
console.log(`${failure.timestamp ?? 'time missing'} ${failure.code ?? failure.type ?? 'failure'} host=${failure.hostId ?? 'unknown'} device=${failure.deviceId ?? 'unknown'} providerSession=${failure.providerSessionId ?? 'unknown'}; ${failure.correlation.causation}`);
|
|
301
302
|
}
|
|
302
303
|
export function registerRunCloudFleet(program) {
|
|
303
|
-
const fleet = program.command('fleet').description('
|
|
304
|
+
const fleet = program.command('fleet').description('Admin fleet telemetry, diagnosis, events, failure evidence and serve-sim release channels');
|
|
304
305
|
fleet.command('survey [hostId]').description('Survey all fleet signals in seconds; authenticated admin reads, no local services')
|
|
305
306
|
.option('--json', 'Structured metrics, diagnosis, observations and coverage')
|
|
306
307
|
.option('--range <range>', 'Alert/input-lag window: 1h, 6h, 24h, 7d', '24h')
|
|
@@ -330,6 +331,19 @@ export function registerRunCloudFleet(program) {
|
|
|
330
331
|
requireFleetReadAccess(result.coverage.reason);
|
|
331
332
|
console.log(JSON.stringify({ coverage: result.coverage, items: result.items.map(eventEvidence) }, null, 2));
|
|
332
333
|
}));
|
|
334
|
+
fleet.command('releases').description('Show the serve-sim release channel each pool runs (admin read)')
|
|
335
|
+
.option('--json', 'Structured channel records')
|
|
336
|
+
.action(opts => action(async () => {
|
|
337
|
+
const { channels, unavailable } = await listReleaseChannels(credentialsClient());
|
|
338
|
+
console.log(opts.json ? JSON.stringify({ unavailable, channels }, null, 2) : `${unavailable ? 'Warning: release storage was partly unavailable\n' : ''}${releaseText(channels)}`);
|
|
339
|
+
}));
|
|
340
|
+
fleet.command('promote <channel> <version>').description('Point one pool (ios|android/dev|prod) at a serve-sim build; audited admin write, no host is touched')
|
|
341
|
+
.requiredOption('--reason <text>', 'Why this build is promoted; kept in the audit history')
|
|
342
|
+
.option('--digest <sha256>', 'Artifact content digest, sha256:<64 hex>').option('--artifact <ref>', 'Where the artifact comes from, e.g. gh-run:<id>')
|
|
343
|
+
.action((channel, version, opts) => action(async () => {
|
|
344
|
+
const release = await promoteReleaseChannel(credentialsClient(), { channel, version, reason: opts.reason, digest: opts.digest, artifact: opts.artifact });
|
|
345
|
+
console.log(`${release.channel} -> ${release.version} (revision ${release.revision}; was ${release.previous?.version ?? 'unset'})`);
|
|
346
|
+
}));
|
|
333
347
|
fleet.command('logs <hostId>').description('Read bounded retained host logs; no SSH or host changes')
|
|
334
348
|
.option('--source <source>', 'service, warm_pool, or event_log', 'service').option('--limit <rows>', 'Maximum lines (1..500)', '100')
|
|
335
349
|
.action((hostId, opts) => action(async () => {
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.2.
|
|
1
|
+
export const CLI_VERSION = '0.2.179';
|
package/package.json
CHANGED
|
Binary file
|
package/runcloud-0.2.177.tgz
DELETED
|
Binary file
|