Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions cli/backup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';
const chalk = require('chalk'),
tools = require('../lib/cmd/dev-tools');

/**
* Configure `clay backup` CLI arguments.
* @param {object} yargs
* @returns {object}
*/
function builder(yargs) {
return yargs
.usage('Usage: $0 backup <url>')
.example('$0 backup https://domain.com/_pages/homepage --output homepage-backup.clay', 'Create page snapshot')
.option('output', {
alias: 'o',
describe: 'output snapshot file path',
type: 'string'
})
.option('json', {
describe: 'output machine-readable json',
type: 'boolean'
});
}

/**
* Create a page snapshot and render output.
* @param {object} argv
* @returns {Promise<void>}
*/
async function handler(argv) {
const result = await tools.backupPage(argv.url, argv.output);

if (argv.json) {
console.log(JSON.stringify(result, null, 2)); // eslint-disable-line no-console
return;
}

console.log(chalk.green(`Backup saved: ${result.filePath}`)); // eslint-disable-line no-console
console.log(`Dispatches: ${result.dispatchCount}`); // eslint-disable-line no-console
console.log(`Page: ${result.resolved.pageUri}`); // eslint-disable-line no-console
}

module.exports = {
command: 'backup <url>',
describe: 'Snapshot a page and its layout dispatches',
aliases: ['snap'],
builder,
handler
};
4 changes: 4 additions & 0 deletions cli/cli-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ module.exports = {
describe: 'export layout when exporting page(s)',
type: 'boolean'
},
yesLayout: {
describe: 'confirm that layout refs may be modified',
type: 'boolean'
},
yaml: {
alias: 'yaml', // -y, --yaml
describe: 'parse bootstrap format',
Expand Down
102 changes: 102 additions & 0 deletions cli/doctor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict';
const chalk = require('chalk'),
options = require('./cli-options'),
{ ensureLayoutConfirmation } = require('./layout-confirmation'),
doctor = require('../lib/cmd/doctor');

/**
* Configure `clay doctor` CLI arguments.
* @param {object} yargs
* @returns {object}
*/
function builder(yargs) {
return yargs
.usage('Usage: $0 doctor <url>')
.example('$0 doctor https://domain.com/_pages/homepage -k qa', 'Diagnose page refs')
.example('$0 doctor https://domain.com/_pages/homepage --fix --apply -k qa', 'Prune missing refs and apply')
.option('k', options.key)
.option('c', options.concurrency)
.option('fix', {
describe: 'run safe auto-fix plan for missing refs',
type: 'boolean'
})
.option('apply', {
describe: 'apply mutations (default is dry-run)',
type: 'boolean'
})
.option('publish', {
describe: 'publish page after apply',
type: 'boolean'
})
.option('layout', {
alias: 'l',
describe: 'include layout refs in checks and mutations',
type: 'boolean'
})
.option('yes-layout', options.yesLayout)
.option('json', {
describe: 'output machine-readable json',
type: 'boolean'
});
}

/**
* Run diagnosis or safe-fix mode and render output.
* @param {object} argv
* @returns {Promise<void>}
*/
async function handler(argv) {
await ensureLayoutConfirmation(argv, 'doctor');

if (argv.fix) {
const result = await doctor.safeFix(argv.url, {
key: argv.key,
apply: argv.apply,
publish: argv.publish,
concurrency: argv.concurrency,
layout: argv.layout
});

if (argv.json) {
console.log(JSON.stringify(result, null, 2)); // eslint-disable-line no-console
return;
}

console.log(chalk.cyan(`Doctor ${result.dryRun ? 'plan' : 'apply'} for ${result.resolved.pageUri}`)); // eslint-disable-line no-console
console.log(`Missing refs: ${result.missingRefs.length}`); // eslint-disable-line no-console
console.log(`Changes: ${result.changes.length}`); // eslint-disable-line no-console
if (result.changes.length) {
result.changes.forEach((change) => console.log(`- ${change.action} ${change.path}`)); // eslint-disable-line no-console
}
return;
}

const diagnosis = await doctor.diagnose(argv.url, {
key: argv.key,
concurrency: argv.concurrency,
layout: argv.layout
});

if (argv.json) {
console.log(JSON.stringify(diagnosis, null, 2)); // eslint-disable-line no-console
return;
}

console.log(chalk.cyan(`Doctor report for ${diagnosis.resolved.pageUri}`)); // eslint-disable-line no-console
console.log(`Refs scanned: ${diagnosis.refsCount}`); // eslint-disable-line no-console
console.log(`Missing refs: ${diagnosis.missingRefs.length}`); // eslint-disable-line no-console
diagnosis.missingRefs.forEach((ref) => console.log(`- ${ref}`)); // eslint-disable-line no-console

if (diagnosis.lintErrors.length) {
console.log(chalk.yellow('\nLint errors:')); // eslint-disable-line no-console
diagnosis.lintErrors.forEach((msg) => console.log(`- ${msg}`)); // eslint-disable-line no-console
}
}

module.exports = {
command: 'doctor <url>',
describe: 'Diagnose and safely repair broken refs on pages',
aliases: ['doc'],
builder,
handler
};
13 changes: 12 additions & 1 deletion cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,24 @@ const yargs = require('yargs'),
notifier = updateNotifier({
pkg
}),
// Map short aliases and full command names to command modules.
commands = {
c: 'compile',
cfg: 'config',
d: 'doctor',
e: 'export',
i: 'import',
l: 'lint',
p: 'pack'
p: 'pack',
rf: 'refs',
b: 'backup',
rs: 'restore',
rsc: 'rescue',
doctor: 'doctor',
refs: 'refs',
backup: 'backup',
restore: 'restore',
rescue: 'rescue'
},
listCommands = Object.keys(commands).concat(Object.values(commands));

Expand Down
48 changes: 48 additions & 0 deletions cli/layout-confirmation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';
const readline = require('readline');

const LAYOUT_CONFIRM_TOKEN = 'LAYOUT';

/**
* Require an explicit confirmation whenever --layout is enabled.
* In non-interactive environments, users must pass --yes-layout.
*
* @param {object} argv
* @param {string} commandName
* @returns {Promise<void>}
*/
async function ensureLayoutConfirmation(argv, commandName) {
if (!argv.layout) return;
if (argv.yesLayout) return;

if (!process.stdin.isTTY || !process.stdout.isTTY) {
throw new Error(`--layout requires confirmation for ${commandName}. Re-run with --yes-layout.`);
}

const answer = await askQuestion(`--layout can modify layout references for ${commandName}. Type ${LAYOUT_CONFIRM_TOKEN} to continue: `);

if (answer !== LAYOUT_CONFIRM_TOKEN) {
throw new Error('Layout confirmation did not match. Aborting without changes.');
}
}

/**
* Ask a single stdin question and resolve with the response string.
* @param {string} prompt
* @returns {Promise<string>}
*/
function askQuestion(prompt) {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});

rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}

module.exports.ensureLayoutConfirmation = ensureLayoutConfirmation;
126 changes: 126 additions & 0 deletions cli/refs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
'use strict';
const _ = require('lodash'),
chalk = require('chalk'),
options = require('./cli-options'),
{ ensureLayoutConfirmation } = require('./layout-confirmation'),
refs = require('../lib/cmd/refs');

/**
* Configure `clay refs` CLI arguments.
* @param {object} yargs
* @returns {object}
*/
function builder(yargs) {
return yargs
.usage('Usage: $0 refs <url-or-prefix>')
.example('$0 refs https://domain.com/_pages/homepage --action prune -k qa', 'Prune missing refs')
.example('$0 refs https://domain.com/_pages/homepage --action replace --ref /_components/a --to /_components/b --apply -k qa', 'Replace refs in page')
.example('$0 refs stg --action where-used --ref /_components/foo/instances/bar -k qa', 'Find pages using ref')
.example('$0 refs stg --action reset --ref /_components/foo/instances/bar --apply -k qa', 'Reset a broken component instance')
.option('k', options.key)
.option('c', options.concurrency)
.option('action', {
describe: 'refs operation',
choices: ['prune', 'replace', 'reset', 'where-used'],
demandOption: true
})
.option('ref', {
describe: 'reference uri for replace/reset/where-used',
type: 'string'
})
.option('to', {
describe: 'replacement ref uri, or {} behavior via literal "{}"',
type: 'string'
})
.option('apply', {
describe: 'apply mutation operations (default dry-run)',
type: 'boolean'
})
.option('publish', {
describe: 'publish page after apply',
type: 'boolean'
})
.option('layout', {
alias: 'l',
describe: 'include layout refs in checks and mutations',
type: 'boolean'
})
.option('yes-layout', options.yesLayout)
.option('where-used', {
describe: 'when resetting, also return pages that reference this ref',
type: 'boolean'
})
.option('size', {
describe: 'max search hits for where-used',
type: 'number',
default: 1000
})
.option('json', {
describe: 'output machine-readable json',
type: 'boolean'
});
}

/**
* Dispatch refs action and print results.
* @param {object} argv
* @returns {Promise<void>}
*/
async function handler(argv) {
await ensureLayoutConfirmation(argv, 'refs');

const result = await runAction(argv);

if (argv.json) {
console.log(JSON.stringify(result, null, 2)); // eslint-disable-line no-console
return;
}

console.log(chalk.cyan(`refs:${result.action}`)); // eslint-disable-line no-console
if (result.missingRefs) console.log(`Missing refs: ${result.missingRefs.length}`); // eslint-disable-line no-console
if (result.changes) console.log(`Changes: ${result.changes.length}`); // eslint-disable-line no-console
if (result.pages) {
console.log(`Pages: ${result.pages.length}`); // eslint-disable-line no-console
result.pages.forEach((page) => console.log(`- ${page}`)); // eslint-disable-line no-console
}
if (result.applied) console.log(chalk.green('Changes applied')); // eslint-disable-line no-console
else if (_.has(result, 'dryRun') && result.dryRun) console.log(chalk.yellow('Dry-run only. Re-run with --apply to mutate data.')); // eslint-disable-line no-console
}

/**
* Validate action-specific required args before execution.
* @param {object} argv
*/
function validateArgs(argv) {
if (argv.action === 'replace' && (!argv.ref || !argv.to)) {
throw new Error('--ref and --to are required for --action replace');
}

if ((argv.action === 'reset' || argv.action === 'where-used') && !argv.ref) {
throw new Error('--ref is required for this action');
}
}

/**
* Route action string to refs command implementation.
* @param {object} argv
* @returns {Promise<object>}
*/
function runAction(argv) {
validateArgs(argv);

switch (argv.action) {
case 'prune': return refs.prune(argv.url, argv);
case 'replace': return refs.replace(argv.url, argv.ref, argv.to, argv);
case 'reset': return refs.reset(argv.ref, argv.url, argv);
default: return refs.whereUsed(argv.url, argv.ref, argv);
}
}

module.exports = {
command: 'refs <url>',
describe: 'Prune, replace, reset, and locate refs',
aliases: ['ref'],
builder,
handler
};
Loading