added directly mode on some commands (#31)

This commit is contained in:
Arwid Thornström
2025-11-06 13:30:10 +01:00
committed by GitHub
parent 1ed0f300dc
commit 7e50d5eb54
4 changed files with 415 additions and 52 deletions
+143 -2
View File
@@ -2,6 +2,54 @@
**pwcli** is a CLI for Photowall platform that provides commands for deployment, AWS operations, workflow automation, and more.
## Quick Command Reference
```
pwcli
├── ai (ai)
│ └── promptcontextlibrary (pcl) ..................... ai pcl
├── aws (a)
│ ├── ecs (e)
│ │ └── sshtestserver (sts) ....................... a e sts
│ ├── logs (l) ....................................... a l
│ ├── parameterstore (ps)
│ │ ├── bulk-create (bc) .......................... a ps bc
│ │ ├── create (c) ................................ a ps c
│ │ ├── list (l) .................................. a ps l
│ │ └── update (u) ................................ a ps u
│ ├── pipeline (pl)
│ │ └── status (s) ................................ a pl s
│ └── s3 (s3)
│ ├── clear_prints (cp) ......................... a s3 cp
│ ├── print_on_station (pos) .................... a s3 pos
│ └── sync_images (si) .......................... a s3 si
├── deployment (d)
│ └── testservers (ts)
│ ├── create (c) ................................ d ts c
│ ├── delete (d) ................................ d ts d
│ ├── status (s) ................................ d ts s
│ └── update (u) ................................ d ts u
├── mandrill (m)
│ └── fix_template (ft) .............................. m ft
├── setup (s)
│ └── settings (se) .................................. s se
├── test (t)
│ └── routes (r) ..................................... t r
├── update (up) ........................................ up
└── workflow (wf)
├── git (g)
│ ├── clean_local_branches (clb) ................ wf g clb
│ └── clean_old_branches (cb) ................... wf g cb
└── setupissue (si) ................................ wf si
```
## Usage
Run `pwcli` to start the interactive menu, or use command shortcuts directly:
@@ -81,12 +129,23 @@ Deployment and test server management
- **create** (alias: `c`) - Create new testserver on AWS
- **delete** (alias: `d`) - Choose a testserver to delete
- **status** (alias: `s`) - Show status for available testservers
- Flags:
- `--json` - Output status information in JSON format (non-interactive)
- **update** (alias: `u`) - Update stack with different branch
- Flags:
- `--branch <branch>` - Specify GitHub branch to deploy
- `--stack <stack>` - Specify test stack (formats: `photowall-test-01`, `test-01`, or `01`)
- `--skip-confirm` - Skip confirmation prompt when using parameters
- `--json` - Output result in JSON format with success flag and message
**Examples:**
- `pwcli d ts s` - View testserver status
- `pwcli d ts s` - View testserver status (interactive)
- `pwcli d ts s --json` - View testserver status as JSON
- `pwcli d ts c` - Create new testserver
- `pwcli d ts u` - Update existing testserver
- `pwcli d ts u` - Update existing testserver (interactive)
- `pwcli d ts u --branch feature/123 --stack test-01` - Update test-01 with feature/123 branch (with confirmation)
- `pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm` - Update without confirmation
- `pwcli d ts u --branch feature/123 --stack 01 --skip-confirm --json` - Update and output JSON result
---
@@ -168,5 +227,87 @@ Settings are stored in `~/.pwcli_settings`
---
## Non-Interactive Mode & JSON Output
Several commands support non-interactive mode with JSON output, making them perfect for CI/CD pipelines and automation scripts.
### Testserver Status with JSON Output
Get testserver status information in JSON format:
```bash
pwcli d ts s --json
```
**JSON Output Format:**
```json
[
{
"name": "photowall-test-01",
"status": "UPDATE_COMPLETE",
"branch": "feature/123-my-feature",
"issueNumber": "123",
"issueStatus": "✅ open",
"issueUpdated": "2025-11-06T10:30:00Z",
"created": "2025-10-15T08:00:00Z",
"updated": "2025-11-05T14:20:00Z",
"url": "https://test-01.photowall-test.com/us"
}
]
```
### Testserver Update - Non-Interactive
Update a testserver without interactive prompts:
```bash
pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm --json
```
**Success Response:**
```json
{
"success": true,
"message": "Successfully updated photowall-test-01 with branch feature/123 and started pipeline",
"stack": "photowall-test-01",
"branch": "feature/123",
"pipeline": "photowall-test-01Pipeline"
}
```
**Error Response:**
```json
{
"success": false,
"message": "Not a valid branch on photowall repo: invalid-branch"
}
```
### Automation Examples
**In a CI/CD Pipeline:**
```bash
#!/bin/bash
# Deploy to testserver and check result
result=$(pwcli d ts u --branch $CI_BRANCH --stack test-01 --skip-confirm --json)
success=$(echo $result | jq -r '.success')
if [ "$success" == "true" ]; then
echo "Deployment successful!"
exit 0
else
echo "Deployment failed!"
exit 1
fi
```
**Check testserver availability:**
```bash
# Get all testservers and find an available one
pwcli d ts s --json | jq '.[] | select(.status == "UPDATE_COMPLETE") | .name'
```
---
For more information, visit the repository or run commands interactively with `pwcli`
+24
View File
@@ -14,3 +14,27 @@ needed for the photowall organisation.
# Usage
When running `pwcli` command in the terminal is will open a interactive view but all commands are accessable through shortcuts. So if you want to run status on development testservers its possible to run `pwcli d ts s` for d = development, ts = test servers, s = status. Shortcuts are listed in the interactive view.
## Interactive Mode
Run commands interactively with prompts and menus:
```bash
pwcli # Open interactive menu
pwcli d ts u # Update testserver with interactive prompts
pwcli d ts s # View testserver status in table format
```
## Non-Interactive Mode
Many commands support non-interactive mode with parameters and JSON output, perfect for CI/CD and automation:
```bash
# View testserver status as JSON
pwcli d ts s --json
# Update testserver without interactive prompts
pwcli d ts u --branch feature/123 --stack test-01 --skip-confirm --json
```
For detailed documentation including all flags, parameters, and JSON output formats, see [docs/help.md](docs/help.md).
@@ -18,6 +18,9 @@ module.exports = {
return;
}
// Check for --json flag
const jsonOutput = toolbox.parameters.options.json;
const testStacks = await getTestStacksInfo(toolbox);
const status = (status: string) => {
switch (status) {
@@ -189,6 +192,33 @@ module.exports = {
return aNumber - bNumber;
});
// If JSON output is requested, output and return early
if (jsonOutput) {
const jsonData = testStacks.map((stack) => {
const regex = /^(\d+)/;
const match = stack.branch.match(regex);
const branchIssueNumber = match ? match[1] : null;
const issueStatus = issueStatuses.find(
(issueStatus) => issueStatus.issue === branchIssueNumber,
);
return {
name: stack.name,
status: stack.status,
branch: stack.branch,
issueNumber: branchIssueNumber,
issueStatus: issueStatus ? issueStatus.status : getStatusIcon('null'),
issueUpdated: issueStatus ? issueStatus.issueUpdated : null,
created: stack.created,
updated: stack.updated,
url: stackUrl(stack.name),
};
});
console.log(JSON.stringify(jsonData, null, 2));
return;
}
for (const stack of testStacks) {
const regex = /^(\d+)/;
const match = stack.branch.match(regex);
@@ -80,9 +80,30 @@ module.exports = {
run: async (toolbox: GluegunMenuToolbox) => {
const { system, strings, print } = toolbox;
// Check for parameters
const jsonOutput = toolbox.parameters.options.json;
const branchParam = toolbox.parameters.options.branch;
const stackParam =
toolbox.parameters.options.stack || toolbox.parameters.options.server;
const skipConfirm = toolbox.parameters.options['skip-confirm'];
// Load settings
const config = await getSettings(toolbox);
if (!config || !config.photowall_repo || config.photowall_repo === '') {
if (jsonOutput) {
console.log(
JSON.stringify(
{
success: false,
message:
'The update command requires that you set an absolute path to your local photowall repository.',
},
null,
2,
),
);
return;
}
print.error(`
The create command requires that you set an absolute path to your local photowall repository.
@@ -93,40 +114,122 @@ module.exports = {
}
let branchName = null;
while (!branchName) {
branchName = await getBranchName(
toolbox,
config.gh_token,
config.photowall_repo,
);
// If branch is provided as parameter, validate it
if (branchParam) {
const octokit = new Octokit({ auth: config.gh_token });
try {
const response = await octokit.request(
'GET /repos/{owner}/{repo}/branches/{branch}',
{
owner: 'photowall',
repo: 'photowall',
branch: branchParam,
},
);
if (response.status === 200) {
branchName = branchParam;
}
} catch (e) {
if (jsonOutput) {
console.log(
JSON.stringify(
{
success: false,
message: `Not a valid branch on photowall repo: ${branchParam}`,
},
null,
2,
),
);
return;
}
print.error(`Not a valid branch on photowall repo: ${branchParam}`);
return;
}
} else {
// Interactive mode - ask for branch
while (!branchName) {
branchName = await getBranchName(
toolbox,
config.gh_token,
config.photowall_repo,
);
}
}
const testStacks = await getTestStacksInfo(toolbox);
const options = testStacks.map((item) => {
return item.name;
});
let stackName = null;
const prompt = new AutoComplete({
name: 'testserver',
message: 'Choose a server',
limit: options.length - 1,
initial: options.length - 1,
choices: options,
});
// If stack is provided as parameter, validate it
if (stackParam) {
const foundStack = testStacks.find((stack) => {
return (
stack.name === stackParam ||
stack.name === `photowall-${stackParam}` ||
stack.name.replace('photowall-', '') === stackParam
);
});
if (foundStack) {
stackName = foundStack.name;
} else {
if (jsonOutput) {
console.log(
JSON.stringify(
{
success: false,
message: `Stack not found: ${stackParam}`,
availableStacks: testStacks.map((s) => s.name),
},
null,
2,
),
);
return;
}
print.error(`Stack not found: ${stackParam}`);
print.info('Available stacks:');
testStacks.forEach((s) => print.info(` - ${s.name}`));
return;
}
} else {
// Interactive mode - ask for stack
const options = testStacks.map((item) => {
return item.name;
});
const prompt = new AutoComplete({
name: 'testserver',
message: 'Choose a server',
limit: options.length - 1,
initial: options.length - 1,
choices: options,
});
stackName = await prompt.run();
}
try {
const stackName = await prompt.run();
const stacksuffix = stackName.replace('photowall-', '');
const shouldRun = await getConfirmation(branchName, stackName);
// Skip confirmation if parameters are provided or skip-confirm flag is set
const shouldRun =
branchParam && stackParam
? skipConfirm
? true
: await getConfirmation(branchName, stackName)
: await getConfirmation(branchName, stackName);
if (shouldRun) {
const cmd = `make update-stack STACK_ENVIRONMENT_NAME=${stacksuffix} GITHUB_BRANCH=${branchName}`;
print.fancy(
`Running [${chalk.yellow(
cmd,
)}] in folder [${`${config.photowall_repo}/cloudformation/`}]`,
);
if (!jsonOutput) {
print.fancy(
`Running [${chalk.yellow(
cmd,
)}] in folder [${`${config.photowall_repo}/cloudformation/`}]`,
);
}
const child = spawn(
'make',
[
@@ -140,10 +243,14 @@ module.exports = {
);
child.stdout.setEncoding('utf8');
child.stdout.on('data', (data) => {
process.stdout.write(data);
if (!jsonOutput) {
process.stdout.write(data);
}
});
child.stderr.on('data', (data) => {
process.stderr.write(data);
if (!jsonOutput) {
process.stderr.write(data);
}
});
await new Promise((resolve, reject) => {
child.on('close', (code) => {
@@ -156,48 +263,109 @@ module.exports = {
});
// Check the status
const spinner = print.spin(`Waiting for stack to update`);
const spinner = jsonOutput
? null
: print.spin(`Waiting for stack to update`);
let currentStackStatus = '';
while (currentStackStatus !== 'UPDATE_COMPLETE') {
const stackCmd = `aws cloudformation describe-stacks --stack-name photowall-${stacksuffix}`;
const response = await system.run(stackCmd, { trim: true });
const stackData = JSON.parse(response);
currentStackStatus = stackData.Stacks[0].StackStatus;
let statusOutput = '';
switch (currentStackStatus) {
case 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS':
statusOutput = `[${chalk.yellow(
currentStackStatus,
)}]: Almost done, cleanup in progress`;
break;
case 'UPDATE_COMPLETE':
statusOutput = `[${chalk.green(
currentStackStatus,
)}]: Update complete`;
break;
default:
statusOutput = `[${chalk.red(
currentStackStatus,
)}]: Updating the stack`;
break;
if (!jsonOutput) {
let statusOutput = '';
switch (currentStackStatus) {
case 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS':
statusOutput = `[${chalk.yellow(
currentStackStatus,
)}]: Almost done, cleanup in progress`;
break;
case 'UPDATE_COMPLETE':
statusOutput = `[${chalk.green(
currentStackStatus,
)}]: Update complete`;
break;
default:
statusOutput = `[${chalk.red(
currentStackStatus,
)}]: Updating the stack`;
break;
}
spinner.text = statusOutput;
}
spinner.text = statusOutput;
if (currentStackStatus !== 'UPDATE_COMPLETE') {
await sleep(1000);
}
}
spinner.text = `Will start the pipeline now [${chalk.yellow(
`photowall-${stacksuffix}Pipeline`,
)}]`;
if (!jsonOutput) {
spinner.text = `Will start the pipeline now [${chalk.yellow(
`photowall-${stacksuffix}Pipeline`,
)}]`;
}
const runPipelineCmd = `aws codepipeline start-pipeline-execution --name photowall-${stacksuffix}Pipeline`;
await system.run(runPipelineCmd);
await sleep(2000);
spinner.stop();
print.fancy('Pipeline started, bye! 🚀');
if (jsonOutput) {
console.log(
JSON.stringify(
{
success: true,
message: `Successfully updated ${stackName} with branch ${branchName} and started pipeline`,
stack: stackName,
branch: branchName,
pipeline: `photowall-${stacksuffix}Pipeline`,
},
null,
2,
),
);
} else {
spinner.stop();
print.fancy('Pipeline started, bye! 🚀');
}
} else {
// User cancelled
if (jsonOutput) {
console.log(
JSON.stringify(
{
success: false,
message: 'Update cancelled by user',
},
null,
2,
),
);
} else {
print.info('Update cancelled');
}
}
} catch (e) {
console.log(`e`, e);
if (jsonOutput) {
console.log(
JSON.stringify(
{
success: false,
message: `Error during update: ${e.message || e}`,
error: e.message || String(e),
},
null,
2,
),
);
} else {
console.log(`e`, e);
}
}
// Skip menu if using parameters or JSON output
if (jsonOutput || (branchParam && stackParam)) {
return;
}
if (toolbox.fromMenu()) {