Skip to content

Commit 1b8d86c

Browse files
Update studio site list CLI command (#2127)
* Update `studio site list` command * Fix tests * Fix lint * Fix tests * Cache return value of `pm2.list` * Remove PHP version from list command * Improve cacheFunctionTTL * Fix tests * Also prune cache
1 parent 4f0bc3c commit 1b8d86c

File tree

16 files changed

+637
-143
lines changed

16 files changed

+637
-143
lines changed

cli/__mocks__/cli-table3.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default class MockTable {
2+
constructor() {}
3+
push() {
4+
return this;
5+
}
6+
toString() {
7+
return 'table output';
8+
}
9+
}

cli/__mocks__/ora.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
jest.mock( 'ora', () => {
2-
return {
3-
__esModule: true,
4-
default: () => ( {
5-
start: jest.fn().mockReturnThis(),
6-
stop: jest.fn().mockReturnThis(),
7-
succeed: jest.fn().mockReturnThis(),
8-
fail: jest.fn().mockReturnThis(),
9-
} ),
10-
};
11-
} );
1+
export default {
2+
start: jest.fn().mockReturnThis(),
3+
stop: jest.fn().mockReturnThis(),
4+
succeed: jest.fn().mockReturnThis(),
5+
fail: jest.fn().mockReturnThis(),
6+
};

cli/commands/preview/list.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import { __, _n, sprintf } from '@wordpress/i18n';
2+
import Table from 'cli-table3';
23
import { PreviewCommandLoggerAction as LoggerAction } from 'common/logger-actions';
4+
import { format } from 'date-fns';
35
import { getAuthToken } from 'cli/lib/appdata';
46
import {
5-
getSnapshotCliJson,
6-
getSnapshotCliTable,
7+
formatDurationUntilExpiry,
78
getSnapshotsFromAppdata,
89
isSnapshotExpired,
910
} from 'cli/lib/snapshots';
11+
import { getColumnWidths } from 'cli/lib/utils';
1012
import { validateSiteFolder } from 'cli/lib/validation';
1113
import { Logger, LoggerError } from 'cli/logger';
1214
import { StudioArgv } from 'cli/types';
1315

14-
export async function runCommand( siteFolder: string, format: 'table' | 'json' ): Promise< void > {
16+
export async function runCommand(
17+
siteFolder: string,
18+
outputFormat: 'table' | 'json'
19+
): Promise< void > {
1520
const logger = new Logger< LoggerAction >();
1621

1722
try {
@@ -46,11 +51,41 @@ export async function runCommand( siteFolder: string, format: 'table' | 'json' )
4651
logger.reportSuccess( snapshotsMessage );
4752
}
4853

49-
if ( format === 'table' ) {
50-
const table = getSnapshotCliTable( snapshots );
54+
if ( outputFormat === 'table' ) {
55+
const colWidths = getColumnWidths( [ 0.4, 0.25, 0.175, 0.175 ] );
56+
const table = new Table( {
57+
head: [ __( 'URL' ), __( 'Site Name' ), __( 'Updated' ), __( 'Expires in' ) ],
58+
wordWrap: true,
59+
wrapOnWordBoundary: false,
60+
colWidths,
61+
style: {
62+
head: [],
63+
border: [],
64+
},
65+
} );
66+
67+
for ( const snapshot of snapshots ) {
68+
const durationUntilExpiry = formatDurationUntilExpiry( snapshot.date );
69+
const url = `https://${ snapshot.url }`;
70+
71+
table.push( [
72+
{ href: url, content: url },
73+
snapshot.name,
74+
format( snapshot.date, 'yyyy-MM-dd HH:mm' ),
75+
durationUntilExpiry,
76+
] );
77+
}
78+
5179
console.log( table.toString() );
5280
} else {
53-
console.log( JSON.stringify( getSnapshotCliJson( snapshots ), null, 2 ) );
81+
const output = snapshots.map( ( snapshot ) => ( {
82+
url: `https://${ snapshot.url }`,
83+
name: snapshot.name,
84+
date: format( snapshot.date, 'yyyy-MM-dd HH:mm' ),
85+
expiresIn: formatDurationUntilExpiry( snapshot.date ),
86+
} ) );
87+
88+
console.log( JSON.stringify( output, null, 2 ) );
5489
}
5590
} catch ( error ) {
5691
if ( error instanceof LoggerError ) {

cli/commands/site/list.ts

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,95 @@
1+
import os from 'node:os';
12
import { __, _n, sprintf } from '@wordpress/i18n';
23
import Table from 'cli-table3';
3-
import { PreviewCommandLoggerAction as LoggerAction } from 'common/logger-actions';
4-
import { readAppdata, type SiteData } from 'cli/lib/appdata';
4+
import { SiteCommandLoggerAction as LoggerAction } from 'common/logger-actions';
5+
import { getSiteUrl, readAppdata, type SiteData } from 'cli/lib/appdata';
6+
import { connect, disconnect } from 'cli/lib/pm2-manager';
7+
import { getColumnWidths } from 'cli/lib/utils';
8+
import { isServerRunning } from 'cli/lib/wordpress-server-manager';
59
import { Logger, LoggerError } from 'cli/logger';
610
import { StudioArgv } from 'cli/types';
711

8-
interface SiteTable {
9-
id: string;
12+
interface SiteListEntry {
13+
status: string;
1014
name: string;
1115
path: string;
12-
phpVersion: string;
16+
url: string;
1317
}
1418

15-
function getSitesCliTable( sites: SiteData[] ) {
16-
const table = new Table( {
17-
head: [ __( 'Name' ), __( 'Path' ), __( 'ID' ), __( 'PHP' ) ],
18-
style: {
19-
head: [ 'cyan' ],
20-
border: [ 'grey' ],
21-
},
22-
wordWrap: true,
23-
wrapOnWordBoundary: false,
24-
} );
19+
function getPrettyPath( path: string ): string {
20+
return path.replace( process.cwd(), '.' ).replace( os.homedir(), '~' );
21+
}
2522

26-
sites.forEach( ( site ) => {
27-
table.push( [ site.name, site.path, site.id, site.phpVersion ] );
28-
} );
23+
async function getSiteListData( sites: SiteData[] ) {
24+
const result: SiteListEntry[] = [];
2925

30-
return table;
31-
}
26+
for await ( const site of sites ) {
27+
const isOnline = await isServerRunning( site.id );
28+
const status = isOnline ? '🟢 Online' : '🔴 Offline';
29+
const url = getSiteUrl( site );
30+
31+
result.push( {
32+
status,
33+
name: site.name,
34+
path: getPrettyPath( site.path ),
35+
url,
36+
} );
37+
}
3238

33-
function getSitesCliJson( sites: SiteData[] ): SiteTable[] {
34-
return sites.map( ( site ) => ( {
35-
id: site.id,
36-
name: site.name,
37-
path: site.path,
38-
phpVersion: site.phpVersion,
39-
} ) );
39+
return result;
4040
}
4141

4242
export async function runCommand( format: 'table' | 'json' ): Promise< void > {
4343
const logger = new Logger< LoggerAction >();
4444

4545
try {
46-
logger.reportStart( LoggerAction.LOAD, __( 'Loading sites…' ) );
46+
logger.reportStart( LoggerAction.LOAD_SITES, __( 'Loading sites…' ) );
4747
const appdata = await readAppdata();
48-
const allSites = appdata.sites;
4948

50-
if ( allSites.length === 0 ) {
49+
if ( appdata.sites.length === 0 ) {
5150
logger.reportSuccess( __( 'No sites found' ) );
5251
return;
5352
}
5453

5554
const sitesMessage = sprintf(
56-
_n( 'Found %d site', 'Found %d sites', allSites.length ),
57-
allSites.length
55+
_n( 'Found %d site', 'Found %d sites', appdata.sites.length ),
56+
appdata.sites.length
5857
);
5958

6059
logger.reportSuccess( sitesMessage );
6160

61+
logger.reportStart( LoggerAction.START_DAEMON, __( 'Connecting to process daemon...' ) );
62+
await connect();
63+
logger.reportSuccess( __( 'Connected to process daemon' ) );
64+
65+
const sitesData = await getSiteListData( appdata.sites );
66+
6267
if ( format === 'table' ) {
63-
const table = getSitesCliTable( allSites );
68+
const colWidths = getColumnWidths( [ 0.1, 0.2, 0.3, 0.4 ] );
69+
70+
const table = new Table( {
71+
head: [ __( 'Status' ), __( 'Name' ), __( 'Path' ), __( 'URL' ) ],
72+
wordWrap: true,
73+
wrapOnWordBoundary: false,
74+
colWidths,
75+
style: {
76+
head: [],
77+
border: [],
78+
},
79+
} );
80+
81+
table.push(
82+
...sitesData.map( ( site ) => [
83+
site.status,
84+
site.name,
85+
site.path,
86+
{ href: new URL( site.url ).toString(), content: site.url },
87+
] )
88+
);
89+
6490
console.log( table.toString() );
6591
} else {
66-
console.log( JSON.stringify( getSitesCliJson( allSites ), null, 2 ) );
92+
console.log( JSON.stringify( sitesData, null, 2 ) );
6793
}
6894
} catch ( error ) {
6995
if ( error instanceof LoggerError ) {
@@ -72,6 +98,8 @@ export async function runCommand( format: 'table' | 'json' ): Promise< void > {
7298
const loggerError = new LoggerError( __( 'Failed to load sites' ), error );
7399
logger.reportError( loggerError );
74100
}
101+
} finally {
102+
disconnect();
75103
}
76104
}
77105

cli/commands/site/start.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ export async function runCommand( siteFolder: string, skipBrowser = false ): Pro
6161
const loggerError = new LoggerError( __( 'Failed to start site infrastructure' ), error );
6262
logger.reportError( loggerError );
6363
}
64-
process.exit( 1 );
6564
} finally {
6665
disconnect();
6766
}

cli/commands/site/tests/list.test.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { readAppdata } from 'cli/lib/appdata';
2+
import { connect, disconnect } from 'cli/lib/pm2-manager';
3+
import { isServerRunning } from 'cli/lib/wordpress-server-manager';
24
import { Logger } from 'cli/logger';
35

46
jest.mock( 'cli/lib/appdata', () => ( {
57
...jest.requireActual( 'cli/lib/appdata' ),
68
getAppdataDirectory: jest.fn().mockReturnValue( '/test/appdata' ),
79
readAppdata: jest.fn(),
810
} ) );
11+
jest.mock( 'cli/lib/pm2-manager' );
12+
jest.mock( 'cli/lib/wordpress-server-manager' );
913
jest.mock( 'cli/logger' );
1014

1115
describe( 'Sites List Command', () => {
@@ -15,11 +19,14 @@ describe( 'Sites List Command', () => {
1519
id: 'site-1',
1620
name: 'Test Site 1',
1721
path: '/path/to/site1',
22+
port: 8080,
1823
},
1924
{
2025
id: 'site-2',
2126
name: 'Test Site 2',
2227
path: '/path/to/site2',
28+
port: 8081,
29+
customDomain: 'my-site.wp.local',
2330
},
2431
],
2532
snapshots: [],
@@ -42,6 +49,9 @@ describe( 'Sites List Command', () => {
4249

4350
( Logger as jest.Mock ).mockReturnValue( mockLogger );
4451
( readAppdata as jest.Mock ).mockResolvedValue( mockAppdata );
52+
( connect as jest.Mock ).mockResolvedValue( undefined );
53+
( disconnect as jest.Mock ).mockResolvedValue( undefined );
54+
( isServerRunning as jest.Mock ).mockResolvedValue( false );
4555
} );
4656

4757
afterEach( () => {
@@ -53,7 +63,7 @@ describe( 'Sites List Command', () => {
5363
await runCommand( 'table' );
5464

5565
expect( readAppdata ).toHaveBeenCalled();
56-
expect( mockLogger.reportStart ).toHaveBeenCalledWith( 'load', 'Loading sites…' );
66+
expect( mockLogger.reportStart ).toHaveBeenCalledWith( 'loadSites', 'Loading sites…' );
5767
expect( mockLogger.reportSuccess ).toHaveBeenCalledWith( 'Found 2 sites' );
5868
} );
5969

@@ -67,7 +77,7 @@ describe( 'Sites List Command', () => {
6777

6878
await runCommand( 'table' );
6979

70-
expect( mockLogger.reportStart ).toHaveBeenCalledWith( 'load', 'Loading sites…' );
80+
expect( mockLogger.reportStart ).toHaveBeenCalledWith( 'loadSites', 'Loading sites…' );
7181
expect( mockLogger.reportSuccess ).toHaveBeenCalledWith( 'No sites found' );
7282
} );
7383

@@ -90,16 +100,16 @@ describe( 'Sites List Command', () => {
90100
JSON.stringify(
91101
[
92102
{
93-
id: 'site-1',
103+
status: '🔴 Offline',
94104
name: 'Test Site 1',
95105
path: '/path/to/site1',
96-
phpVersion: undefined,
106+
url: 'http://localhost:8080',
97107
},
98108
{
99-
id: 'site-2',
109+
status: '🔴 Offline',
100110
name: 'Test Site 2',
101111
path: '/path/to/site2',
102-
phpVersion: undefined,
112+
url: 'http://my-site.wp.local',
103113
},
104114
],
105115
null,

cli/commands/site/tests/start.test.ts

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,7 @@ describe( 'Site Start Command', () => {
116116
} );
117117

118118
const { runCommand } = await import( '../start' );
119-
120-
await expect( async () => {
121-
await runCommand( mockSiteFolder );
122-
} ).rejects.toThrow( 'process.exit called' );
119+
await runCommand( mockSiteFolder );
123120

124121
expect( mockLogger.reportError ).toHaveBeenCalledWith( expect.any( LoggerError ) );
125122
expect( disconnect ).toHaveBeenCalled();
@@ -133,10 +130,7 @@ describe( 'Site Start Command', () => {
133130
( connect as jest.Mock ).mockRejectedValue( new Error( 'PM2 connection failed' ) );
134131

135132
const { runCommand } = await import( '../start' );
136-
137-
await expect( async () => {
138-
await runCommand( mockSiteFolder );
139-
} ).rejects.toThrow( 'process.exit called' );
133+
await runCommand( mockSiteFolder );
140134

141135
expect( mockLogger.reportStart ).toHaveBeenCalledWith(
142136
'startDaemon',
@@ -154,10 +148,7 @@ describe( 'Site Start Command', () => {
154148
( startWordPressServer as jest.Mock ).mockRejectedValue( new Error( 'Server start failed' ) );
155149

156150
const { runCommand } = await import( '../start' );
157-
158-
await expect( async () => {
159-
await runCommand( mockSiteFolder );
160-
} ).rejects.toThrow( 'process.exit called' );
151+
await runCommand( mockSiteFolder );
161152

162153
expect( mockLogger.reportStart ).toHaveBeenCalledWith(
163154
'startSite',
@@ -175,10 +166,7 @@ describe( 'Site Start Command', () => {
175166
( addDomainToHosts as jest.Mock ).mockRejectedValue( new Error( 'Hosts file error' ) );
176167

177168
const { runCommand } = await import( '../start' );
178-
179-
await expect( async () => {
180-
await runCommand( mockSiteFolder );
181-
} ).rejects.toThrow( 'process.exit called' );
169+
await runCommand( mockSiteFolder );
182170

183171
expect( mockLogger.reportStart ).toHaveBeenCalledWith(
184172
'addDomainToHosts',
@@ -198,10 +186,7 @@ describe( 'Site Start Command', () => {
198186
);
199187

200188
const { runCommand } = await import( '../start' );
201-
202-
await expect( async () => {
203-
await runCommand( mockSiteFolder );
204-
} ).rejects.toThrow( 'process.exit called' );
189+
await runCommand( mockSiteFolder );
205190

206191
expect( mockLogger.reportStart ).toHaveBeenCalledWith(
207192
'generateCert',
@@ -408,10 +393,7 @@ describe( 'Site Start Command', () => {
408393
( readAppdata as jest.Mock ).mockRejectedValue( new Error( 'Appdata error' ) );
409394

410395
const { runCommand } = await import( '../start' );
411-
412-
await expect( async () => {
413-
await runCommand( mockSiteFolder );
414-
} ).rejects.toThrow( 'process.exit called' );
396+
await runCommand( mockSiteFolder );
415397

416398
expect( disconnect ).toHaveBeenCalled();
417399
} );

0 commit comments

Comments
 (0)