Skip to content

Commit 8919abf

Browse files
committed
feat: make CLI agent-compatible with improved non-interactive support
- Add --help examples to all 15 commands that were missing them - Fix actionable error messages with concrete example invocations - Fix typo --allow--manifest-override (double dash) in deploy - Add missing --allow-postgres-deletion flag to deploy command - Fix init to fail fast when --template is missing in non-interactive mode - Fix logs pagination to auto-fetch all pages in non-interactive mode - Add interactive squid selection prompt to view command - Add --json flag to list command for machine-parseable output - Clean up --json output in view/list to strip manifest, packageJson, metrics - Add structured success output (squid, deploy_id, duration) to deploy/remove/restart - Improve --interactive flag description for clarity Made-with: Cursor
1 parent 4d26681 commit 8919abf

19 files changed

Lines changed: 289 additions & 37 deletions

src/command.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ import chalk from 'chalk';
44
import inquirer from 'inquirer';
55
import { isNil, uniqBy } from 'lodash';
66

7-
import { ApiError, getOrganization, getSquid, listOrganizations, listUserSquids, SquidRequest } from './api';
7+
import {
8+
ApiError,
9+
getOrganization,
10+
getSquid,
11+
listOrganizations,
12+
listSquids,
13+
listUserSquids,
14+
Squid,
15+
SquidRequest,
16+
} from './api';
817
import { getTTY } from './tty';
918
import { formatSquidReference, printSquid } from './utils';
1019

@@ -13,7 +22,7 @@ export const SUCCESS_CHECK_MARK = chalk.green('✓');
1322
export abstract class CliCommand extends Command {
1423
static baseFlags = {
1524
interactive: Flags.boolean({
16-
description: 'Disable interactive mode',
25+
description: 'Enable interactive mode. Use --no-interactive to disable prompts for CI/scripts.',
1726
required: false,
1827
default: true,
1928
allowNo: true,
@@ -32,7 +41,6 @@ export abstract class CliCommand extends Command {
3241
this.log(chalk.dim(message));
3342
}
3443

35-
// Haven't find a way to do it with native settings
3644
validateSquidNameFlags(flags: { reference?: any; name?: any }) {
3745
if (flags.reference || flags.name) return;
3846

@@ -41,7 +49,13 @@ export abstract class CliCommand extends Command {
4149
{
4250
name: 'squid name',
4351
validationFn: 'validateSquidName',
44-
reason: 'One of the following must be provided: --reference, --name',
52+
reason: [
53+
'One of the following must be provided: --reference or --name',
54+
'',
55+
'Examples:',
56+
' sqd <command> --reference my-squid@v1',
57+
' sqd <command> --name my-squid --slot <slot>',
58+
].join('\n'),
4559
status: 'failed',
4660
},
4761
],
@@ -165,6 +179,53 @@ export abstract class CliCommand extends Command {
165179
return await this.getOrganizationPrompt(organizations, { using, interactive });
166180
}
167181

182+
async promptSquid(organization: { code: string }, { interactive }: { interactive?: boolean } = {}): Promise<Squid> {
183+
const squids = await listSquids({ organization });
184+
185+
if (squids.length === 0) {
186+
return this.error(`No squids found in organization "${organization.code}".`);
187+
}
188+
189+
if (squids.length === 1) {
190+
return squids[0];
191+
}
192+
193+
const { stdin, stdout } = getTTY();
194+
if (!stdin || !stdout || !interactive) {
195+
return this.error(
196+
[
197+
`Organization "${organization.code}" has ${squids.length} squids:`,
198+
...squids.map((s) => ` - ${formatSquidReference({ name: s.name, slot: s.slot })}`),
199+
``,
200+
`Please specify the squid using "--reference" or "--name" flag.`,
201+
`Example: sqd <command> --reference ${squids[0].name}@${squids[0].slot}`,
202+
].join('\n'),
203+
);
204+
}
205+
206+
const prompt = inquirer.createPromptModule({ input: stdin, output: stdout });
207+
const { squid } = await prompt([
208+
{
209+
name: 'squid',
210+
type: 'list',
211+
message: 'Please choose a squid:',
212+
choices: squids.map((s) => {
213+
const ref = formatSquidReference({ name: s.name, slot: s.slot });
214+
const tags = s.tags.length ? ` (${s.tags.map((t) => t.name).join(', ')})` : '';
215+
return {
216+
name: `${ref}${tags}`,
217+
value: s,
218+
};
219+
}),
220+
},
221+
]);
222+
223+
stdin.destroy();
224+
stdout.destroy();
225+
226+
return squid;
227+
}
228+
168229
private async getOrganizationPrompt<T extends { code: string; name: string }>(
169230
organizations: T[],
170231
{
@@ -186,8 +247,10 @@ export abstract class CliCommand extends Command {
186247
return this.error(
187248
[
188249
`You have ${organizations.length} organizations:`,
189-
...organizations.map((o) => `${chalk.dim(' - ')}${chalk.dim(o.code)}`),
190-
`Please specify one of them explicitly ${using}`,
250+
...organizations.map((o) => ` - ${o.code}`),
251+
``,
252+
`Please specify one of them explicitly ${using}.`,
253+
`Example: sqd <command> --org ${organizations[0].code}`,
191254
].join('\n'),
192255
);
193256
}

src/commands/auth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { DEFAULT_API_URL, setConfig } from '../config';
77
export default class Auth extends CliCommand {
88
static description = `Log in to the Cloud`;
99

10+
static examples = ['sqd auth -k sqd_xyz123...'];
11+
1012
static flags = {
1113
key: Flags.string({
1214
char: 'k',

src/commands/deploy.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ export default class Deploy extends DeployCommand {
149149
required: false,
150150
default: false,
151151
}),
152+
'allow-postgres-deletion': Flags.boolean({
153+
description: 'Allow deleting an existing Postgres addon when deploying a manifest without one',
154+
required: false,
155+
default: false,
156+
}),
152157
};
153158

154159
async run(): Promise<void> {
@@ -160,6 +165,7 @@ export default class Deploy extends DeployCommand {
160165
'hard-reset': hardReset,
161166
'stream-logs': streamLogs,
162167
'add-tag': addTag,
168+
'allow-postgres-deletion': allowPostgresDeletion,
163169
reference,
164170
...flags
165171
},
@@ -272,7 +278,7 @@ export default class Deploy extends DeployCommand {
272278
/**
273279
* Warn if the existing squid has a Postgres addon but the new manifest removes it
274280
*/
275-
if (!hardReset && target?.addons?.postgres && !manifest.deploy?.addons?.postgres) {
281+
if (!hardReset && target?.addons?.postgres && !manifest.deploy?.addons?.postgres && !allowPostgresDeletion) {
276282
const confirmed = await this.promptPostgresDeletion(target, { interactive });
277283
if (!confirmed) return;
278284
}
@@ -307,6 +313,12 @@ export default class Deploy extends DeployCommand {
307313
const deployment = await this.pollDeploy({ organization, deploy });
308314
if (!deployment || !deployment.squid) return;
309315

316+
const squidRef = formatSquidReference({
317+
org: deployment.organization.code,
318+
name: deployment.squid.name,
319+
slot: deployment.squid.slot,
320+
});
321+
310322
if (target) {
311323
this.logDeployResult(UPDATE_COLOR, `The squid ${printSquid(target)} has been successfully updated`);
312324
} else {
@@ -316,6 +328,10 @@ export default class Deploy extends DeployCommand {
316328
);
317329
}
318330

331+
this.log(`squid: ${squidRef}`);
332+
this.log(`deploy_id: ${deployment.id}`);
333+
this.log(`duration: ${Math.round(deployment.totalElapsedTimeMs / 1000)}s`);
334+
319335
if (streamLogs) {
320336
await this.streamLogs({ organization: deployment.organization, squid: deployment.squid });
321337
}
@@ -344,7 +360,13 @@ export default class Deploy extends DeployCommand {
344360
if (interactive) {
345361
this.warn(warning.join('\n'));
346362
} else {
347-
this.error([...warning, `Please do it explicitly ${using}`].join('\n'));
363+
this.error(
364+
[
365+
...warning,
366+
`Please do it explicitly ${using}.`,
367+
`Example: sqd deploy . --name ${squid.name} --allow-update`,
368+
].join('\n'),
369+
);
348370
}
349371
350372
const { confirm } = await inquirer.prompt([
@@ -362,7 +384,7 @@ export default class Deploy extends DeployCommand {
362384
private async promptOverrideConflict(
363385
dest: string,
364386
src: string,
365-
{ using = 'using "--allow--manifest-override" flag', interactive }: { using?: string; interactive?: boolean } = {},
387+
{ using = 'using "--allow-manifest-override" flag', interactive }: { using?: string; interactive?: boolean } = {},
366388
) {
367389
const warning = [
368390
'Conflict detected!',
@@ -375,7 +397,9 @@ export default class Deploy extends DeployCommand {
375397
if (interactive) {
376398
this.warn(warning);
377399
} else {
378-
this.error([warning, `Please do it explicitly ${using}`].join('\n'));
400+
this.error(
401+
[warning, `Please do it explicitly ${using}.`, `Example: sqd deploy . --allow-manifest-override`].join('\n'),
402+
);
379403
}
380404
381405
this.log(
@@ -400,7 +424,13 @@ export default class Deploy extends DeployCommand {
400424
const warning = `The new manifest does not include "addons.postgres", but the squid ${printSquid(squid)} currently has a Postgres database. Deploying will permanently delete the database and all its data.`;
401425

402426
if (!interactive) {
403-
this.error([warning, `Please do it explicitly ${using}`].join('\n'));
427+
this.error(
428+
[
429+
warning,
430+
`Please do it explicitly using "--allow-postgres-deletion" flag.`,
431+
`Example: sqd deploy . --allow-postgres-deletion`,
432+
].join('\n'),
433+
);
404434
}
405435

406436
this.warn(warning);
@@ -428,7 +458,9 @@ export default class Deploy extends DeployCommand {
428458
if (interactive) {
429459
this.warn(warning);
430460
} else {
431-
this.error([warning, `Please specify it explicitly ${using}`].join('\n'));
461+
this.error(
462+
[warning, `Please specify it explicitly ${using}.`, `Example: sqd deploy . --name my-squid`].join('\n'),
463+
);
432464
}
433465
434466
const { input } = await inquirer.prompt([

src/commands/deploy.unit.spec.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ describe('Deploy', () => {
3333
? { postgres: { connections: [], disk: { usageStatus: 'NORMAL', usedBytes: 0, totalBytes: 0 } } }
3434
: undefined,
3535
manifest: {
36-
current: pgVersion
37-
? { deploy: { addons: { postgres: { version: pgVersion } } } }
38-
: { deploy: {} },
36+
current: pgVersion ? { deploy: { addons: { postgres: { version: pgVersion } } } } : { deploy: {} },
3937
raw: '',
4038
},
4139
} as any;

src/commands/gateways/list.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ export default class Ls extends CliCommand {
1111

1212
static description = 'List available gateways';
1313

14+
static examples = [
15+
'sqd gateways list',
16+
'sqd gateways list --type evm',
17+
'sqd gateways list --type evm --name ethereum --chain 1',
18+
];
19+
1420
static flags = {
1521
type: Flags.string({
1622
char: 't',

src/commands/init.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ const SQUID_TEMPLATE_DESC = [
8080
export default class Init extends CliCommand {
8181
static description = 'Setup a new squid project from a template or github repo';
8282

83+
static examples = [
84+
'sqd init my-squid --template evm',
85+
'sqd init my-squid -t substrate',
86+
'sqd init my-squid -t https://github.com/user/repo -d ./target-dir',
87+
'sqd init my-squid -t evm -r',
88+
];
89+
8390
static args = {
8491
name: Args.string({ description: SQUID_NAME_DESC.join('\n'), required: true }),
8592
};
@@ -105,7 +112,7 @@ export default class Init extends CliCommand {
105112
async run() {
106113
const {
107114
args: { name },
108-
flags: { template, dir, remove },
115+
flags: { template, dir, remove, interactive },
109116
} = await this.parse(Init);
110117

111118
const localDir = path.resolve(dir || name);
@@ -122,6 +129,18 @@ export default class Init extends CliCommand {
122129

123130
let resolvedTemplate = template || '';
124131
if (!template) {
132+
if (!interactive) {
133+
const templateNames = Object.keys(TEMPLATE_ALIASES).join(', ');
134+
return this.error(
135+
[
136+
`No template specified.`,
137+
`Available templates: ${templateNames}`,
138+
``,
139+
`Example: sqd init ${name} --template evm`,
140+
].join('\n'),
141+
);
142+
}
143+
125144
const { alias } = await inquirer.prompt({
126145
name: 'alias',
127146
message: `Please select one of the templates for your "${name}" squid:`,

src/commands/list.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@ import { ux as CliUx, Flags } from '@oclif/core';
33
import { listSquids } from '../api';
44
import { CliCommand, SqdFlags } from '../command';
55
import { printSquid } from '../utils';
6+
import { formatSquidJson } from './view';
67

78
export default class List extends CliCommand {
89
static aliases = ['ls'];
910

1011
static description = 'List squids deployed to the Cloud';
1112

13+
static examples = [
14+
'sqd list --org my-org',
15+
'sqd ls --org my-org --name my-squid --tag prod',
16+
'sqd list --org my-org --json',
17+
];
18+
1219
static flags = {
1320
org: SqdFlags.org({
1421
required: false,
@@ -34,11 +41,14 @@ export default class List extends CliCommand {
3441
default: false,
3542
allowNo: true,
3643
}),
44+
json: Flags.boolean({
45+
description: 'Output in JSON format',
46+
}),
3747
};
3848

3949
async run(): Promise<void> {
4050
const {
41-
flags: { truncate, reference, interactive, ...flags },
51+
flags: { truncate, json, reference, interactive, ...flags },
4252
} = await this.parse(List);
4353

4454
const { org, name, slot, tag } = reference ? reference : (flags as any);
@@ -51,6 +61,11 @@ export default class List extends CliCommand {
5161
if (tag || slot) {
5262
squids = squids.filter((s) => s.slot === slot || s.tags.some((t) => t.name === tag));
5363
}
64+
65+
if (json) {
66+
return this.log(JSON.stringify(squids.map(formatSquidJson), null, 2));
67+
}
68+
5469
if (squids.length) {
5570
CliUx.ux.table(
5671
squids,

src/commands/logs.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ function parseDate(str: string): Date {
2424
export default class Logs extends CliCommand {
2525
static description = 'Fetch logs from a squid deployed to the Cloud';
2626

27+
static examples = [
28+
'sqd logs --reference my-squid@v1 --org my-org',
29+
'sqd logs --name my-squid --slot abc123 --since 2h',
30+
'sqd logs --reference my-squid@v1 -f',
31+
'sqd logs --reference my-squid@v1 --level error --container processor',
32+
];
33+
2734
static flags = {
2835
org: SqdFlags.org({
2936
required: false,
@@ -116,6 +123,7 @@ export default class Logs extends CliCommand {
116123
return;
117124
}
118125
let cursor = undefined;
126+
let isFirstPage = true;
119127
do {
120128
const { hasLogs, nextPage }: LogResult = await this.fetchLogs({
121129
organization,
@@ -129,11 +137,12 @@ export default class Logs extends CliCommand {
129137
search,
130138
},
131139
});
132-
if (!hasLogs) {
140+
if (!hasLogs && isFirstPage) {
133141
this.log('No logs found');
134142
return;
135143
}
136-
if (nextPage) {
144+
isFirstPage = false;
145+
if (nextPage && interactive) {
137146
const more = await CliUx.ux.prompt(`type "it" to fetch more logs...`);
138147
if (more !== 'it') {
139148
return;

0 commit comments

Comments
 (0)