@newly/cli 0.2.201 → 0.2.203

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.
@@ -7,7 +7,7 @@ export function releaseControlRequest(file) {
7
7
  const value = JSON.parse(readFileSync(file, 'utf8'));
8
8
  if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.reason !== 'string' || !value.reason.trim()
9
9
  || !Number.isSafeInteger(value.expectedRevision) || Number(value.expectedRevision) < 0
10
- || !['bootstrap', 'enroll', 'announce', 'override', 'pause', 'resume', 'rollback', 'defer'].includes(String(value.action))) {
10
+ || !['bootstrap', 'enroll', 'announce', 'override', 'record-activation', 'pause', 'resume', 'rollback', 'defer'].includes(String(value.action))) {
11
11
  throw new Error('Request needs action, reason and expectedRevision from the current release-control status');
12
12
  }
13
13
  return value;
@@ -21,6 +21,6 @@ export function releaseText(channels) {
21
21
  const channel = channels.find(item => item.channel === name);
22
22
  return channel
23
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');
24
+ : `${name.padEnd(13)} unset`;
25
+ }).join('\n') + '\nRetired channels: expectations and targets come only from `runcloud fleet release-control status`.';
26
26
  }
@@ -334,13 +334,13 @@ export function registerRunCloudFleet(program) {
334
334
  requireFleetReadAccess(result.coverage.reason);
335
335
  console.log(JSON.stringify({ coverage: result.coverage, items: result.items.map(eventEvidence) }, null, 2));
336
336
  }));
337
- fleet.command('releases').description('Show the serve-sim release channel each pool runs (admin read)')
337
+ fleet.command('releases').description('Show the retired serve-sim release channels as history; targets live in `fleet release-control status` (admin read)')
338
338
  .option('--json', 'Structured channel records')
339
339
  .action(opts => action(async () => {
340
340
  const { channels, unavailable } = await listReleaseChannels(credentialsClient());
341
341
  console.log(opts.json ? JSON.stringify({ unavailable, channels }, null, 2) : `${unavailable ? 'Warning: release storage was partly unavailable\n' : ''}${releaseText(channels)}`);
342
342
  }));
343
- 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')
343
+ fleet.command('promote <channel> <version>').description('Retired: the API refuses channel promotions. Announce a rollout or record an activation with `fleet release-control apply`')
344
344
  .requiredOption('--reason <text>', 'Why this build is promoted; kept in the audit history')
345
345
  .option('--digest <sha256>', 'Artifact content digest, sha256:<64 hex>').option('--artifact <ref>', 'Where the artifact comes from, e.g. gh-run:<id>')
346
346
  .action((channel, version, opts) => action(async () => {
@@ -1,9 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
3
  import { ApiClient, friendlyApiError } from '../api.js';
4
4
  import { requireCredentials, resolveProject } from '../config.js';
5
5
  import { SUPPORT_CHAT_TSX } from './supportChat.generated.js';
6
6
  export const SUPPORT_CONFIG = 'src/lib/newly-support.json';
7
+ export const SUPPORT_ARTICLES_DIR = 'support/articles';
7
8
  export const SUPPORT_CHAT = 'src/components/SupportChat.tsx';
8
9
  export function writeSupportChat(dir, force = false) {
9
10
  const path = join(dir, SUPPORT_CHAT);
@@ -104,6 +105,64 @@ export async function runArticlesList(client, projectId, log) {
104
105
  for (const a of articles)
105
106
  log(`${a.id} ${a.title} (updated ${a.updated_at})`);
106
107
  }
108
+ const slugOf = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60).replace(/-+$/, '') || 'article';
109
+ const INDEX = 'articles.json';
110
+ function exportedFiles(dir) {
111
+ try {
112
+ const { articles } = JSON.parse(readFileSync(join(dir, INDEX), 'utf8'));
113
+ return (articles ?? []).map((a) => a.file).filter((f) => typeof f === 'string' && /^[a-z0-9-]+\.md$/.test(f));
114
+ }
115
+ catch {
116
+ return [];
117
+ }
118
+ }
119
+ export const WEBSITE_ARTICLES = 'support-articles.json';
120
+ export async function runArticlesExport(client, projectId, dir, log, websiteDir) {
121
+ const { settings } = (await client.get(`${base(projectId)}/settings`));
122
+ const websiteCopy = websiteDir ? join(websiteDir, WEBSITE_ARTICLES) : null;
123
+ if (!settings?.enabled) {
124
+ if (websiteCopy && existsSync(websiteCopy))
125
+ unlinkSync(websiteCopy);
126
+ log('Nothing exported: the help chat is off for this app. Turn it on with newly support enable first.');
127
+ return;
128
+ }
129
+ const { articles } = (await client.get(`${base(projectId)}/articles`));
130
+ const sorted = [...articles].sort((a, b) => a.title.localeCompare(b.title) || a.id.localeCompare(b.id));
131
+ const owned = new Set(exportedFiles(dir));
132
+ const taken = new Set();
133
+ const free = (file) => !taken.has(file) && (owned.has(file) || !existsSync(join(dir, file)));
134
+ const entries = sorted.map((a) => {
135
+ const slug = slugOf(a.title);
136
+ let file = `${slug}.md`;
137
+ for (let n = 2; !free(file); n++)
138
+ file = `${slug}-${n}.md`;
139
+ taken.add(file);
140
+ return {
141
+ id: a.id, file, title: a.title, body: a.body, source: a.source ?? 'manual',
142
+ source_name: a.source_name ?? null, source_url: a.source_url ?? null, updated_at: a.updated_at,
143
+ };
144
+ });
145
+ mkdirSync(dir, { recursive: true });
146
+ for (const stale of owned) {
147
+ if (!taken.has(stale) && existsSync(join(dir, stale)))
148
+ unlinkSync(join(dir, stale));
149
+ }
150
+ for (const e of entries) {
151
+ const header = [`id: ${e.id}`, `title: ${JSON.stringify(e.title)}`, `source: ${e.source}`];
152
+ if (e.source_name)
153
+ header.push(`source_name: ${JSON.stringify(e.source_name)}`);
154
+ if (e.source_url)
155
+ header.push(`source_url: ${JSON.stringify(e.source_url)}`);
156
+ header.push(`updated_at: ${e.updated_at}`);
157
+ writeFileSync(join(dir, e.file), `---\n${header.join('\n')}\n---\n\n${e.body.trimEnd()}\n`);
158
+ }
159
+ const note = 'Exported from Newly customer support. Edit articles in Newly; newly support articles export overwrites this folder.';
160
+ const index = `${JSON.stringify({ note, articles: entries }, null, 2)}\n`;
161
+ writeFileSync(join(dir, INDEX), index);
162
+ if (websiteDir && websiteCopy && existsSync(websiteDir))
163
+ writeFileSync(websiteCopy, index);
164
+ log(`Exported ${entries.length} ${entries.length === 1 ? 'article' : 'articles'} to ${dir}`);
165
+ }
107
166
  const bodyOf = (opts) => {
108
167
  if (opts.file)
109
168
  return readFileSync(opts.file, 'utf8');
@@ -266,6 +325,12 @@ export function registerSupport(program) {
266
325
  const c = ctx(o.project);
267
326
  await runArticleImportUrl(c.client, c.projectId, url, Boolean(o.refresh), log);
268
327
  }));
328
+ articles.command('export').description('Write the articles into the repo (markdown plus articles.json) for the website FAQ')
329
+ .option('--dir <dir>', 'output folder', SUPPORT_ARTICLES_DIR).option('--project <id>')
330
+ .action((o) => run(async () => {
331
+ const c = ctx(o.project);
332
+ await runArticlesExport(c.client, c.projectId, resolve(o.dir), log, resolve('website'));
333
+ }));
269
334
  articles.command('remove <id>').option('--project <id>').action((id, o) => run(async () => {
270
335
  const c = ctx(o.project);
271
336
  await runArticleRemove(c.client, c.projectId, id, log);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.2.201';
1
+ export const CLI_VERSION = '0.2.203';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newly/cli",
3
- "version": "0.2.201",
3
+ "version": "0.2.203",
4
4
  "description": "Develop, provision, and deploy Newly backends from your terminal",
5
5
  "homepage": "https://newly.app",
6
6
  "repository": {