Skip to content

Commit

Permalink
feat: implement interactive/non-interactive contexts; add task and utils
Browse files Browse the repository at this point in the history
  • Loading branch information
rafamel committed Apr 7, 2021
1 parent df676d6 commit 756fab7
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 6 deletions.
9 changes: 4 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"as-table": "^1.0.55",
"chalk": "^4.1.0",
"chokidar": "^3.5.1",
"ci-info": "^1.6.0",
"cli-belt": "^1.0.3",
"common-tags": "^1.8.0",
"debounce": "^1.2.1",
Expand Down
6 changes: 5 additions & 1 deletion src/cli/bin/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function main(argv: string[], options: Required<CLI.Options>): Task {
-e, --env <value> Environment variables
--level <value> Logging level
--prefix Prefix tasks output with their route
--non-interactive Set the context as non-interactive
-h, --help Show help
-v, --version Show version number
Expand Down Expand Up @@ -67,6 +68,7 @@ export function main(argv: string[], options: Required<CLI.Options>): Task {
'--env': [String] as [StringConstructor],
'--level': String,
'--prefix': Boolean,
'--non-interactive': Boolean,
'--help': Boolean,
'--version': Boolean
};
Expand All @@ -90,6 +92,7 @@ export function main(argv: string[], options: Required<CLI.Options>): Task {
file: cmd['--file'],
directory: cmd['--dir'],
prefix: cmd['--prefix'],
nonInteractive: cmd['--non-interactive'],
level:
cmd['--level'] &&
constants.collections.levels.includes(cmd['--level'].toLowerCase())
Expand Down Expand Up @@ -186,9 +189,10 @@ export function main(argv: string[], options: Required<CLI.Options>): Task {
};
},
context.bind(null, {
env: opts.env,
level: opts.level,
prefix: opts.prefix,
env: opts.env
interactive: !opts.nonInteractive
})
);
}
4 changes: 4 additions & 0 deletions src/definitions/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export interface Context {
* with the stringification of its route.
*/
readonly prefix: PrefixPolicy | boolean;
/**
* Sets a context as non-interactive.
*/
readonly interactive: boolean;
/**
* A *Promise* representing a task's
* cancellation token.
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/create-context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Context } from '../definitions';
import { constants } from '../constants';
import { into } from 'pipettes';
import { TypeGuard } from 'type-core';

const cancellation = new Promise<void>(() => undefined);

Expand All @@ -15,6 +16,9 @@ export function createContext(context?: Partial<Context>): Context {
level: context.level || constants.defaults.level,
route: context.route || [],
prefix: context.prefix || false,
interactive: TypeGuard.isUndefined(context.interactive)
? true
: context.interactive,
cancellation: context.cancellation
? context.cancellation.then(() => undefined)
: cancellation
Expand Down
1 change: 1 addition & 0 deletions src/tasks/stdio/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './announce';
export * from './clear';
export * from './interactive';
export * from './log';
export * from './print';
export * from './select';
35 changes: 35 additions & 0 deletions src/tasks/stdio/interactive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Task, Context } from '../../definitions';
import { isInteractive } from '../../utils/is-interactive';
import { run } from '../../utils/run';
import { raises } from '../exception/raises';
import { log } from './log';
import { Empty } from 'type-core';
import { into } from 'pipettes';

/**
* Marks a task as interactive.
* Will error on non-interactive environments
* unless an `alternate` task is provided.
* @returns Task
*/
export function interactive(task: Task, alternate: Task | Empty): Task.Async {
return async (ctx: Context): Promise<void> => {
const interactive = isInteractive(ctx);

into(
ctx,
log(
'debug',
interactive ? 'Interactive' : 'Non-interactive',
'environment detected'
)
);

return run(
interactive
? task
: alternate || raises(Error('Non-interactive environment detected')),
ctx
);
};
}
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './fetch';
export * from './is-cancelled';
export * from './is-ci';
export * from './is-interactive';
export * from './recreate';
export * from './run';
32 changes: 32 additions & 0 deletions src/utils/is-ci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Context } from '../definitions';
import { TypeGuard } from 'type-core';
import vendors from 'ci-info/vendors.json';

/**
* Returns `true` when a context's environment
* variables indicate it's running in a CI.
*/
export function isCI(context: Context): boolean {
if ('CI' in context.env || 'CONTINUOUS_INTEGRATION' in context.env) {
return true;
}

const arr = vendors.map((vendor) => vendor.env);

for (const env of arr) {
if (TypeGuard.isString(env)) {
if (env in context.env) return true;
} else if (TypeGuard.isArray(env)) {
const present = env.filter((env) => env in context.env);
if (present.length === env.length) return true;
} else {
const entries = Object.entries(env);
const matches = entries.filter(
([key, value]) => context.env[key] === value
);
if (entries.length === matches.length) return true;
}
}

return false;
}
20 changes: 20 additions & 0 deletions src/utils/is-interactive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Context } from '../definitions';
import { isCI } from './is-ci';

/**
* Returns `true` when a context belongs to an interactive
* environment.
* Ensures that the context interactive property is `true`,
* the stdout is a non-dumb *TTY*, and it's not running in a CI.
*/
export function isInteractive(context: Context): boolean {
if (!context.interactive) return false;

const stdout = context.stdio[1] as NodeJS.WriteStream;
if (!stdout || !stdout.isTTY) return false;

if (context.env.TERM === 'dumb') return false;
if (isCI(context)) return false;

return true;
}

0 comments on commit 756fab7

Please sign in to comment.