Skip to content

Commit 828a015

Browse files
committed
feat: unified autocomplete entrypoint
Signed-off-by: Michael Molisani <[email protected]> Signed-off-by: Michael Molisani <[email protected]>
1 parent 7226fb9 commit 828a015

File tree

52 files changed

+1288
-898
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1288
-898
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,5 @@ tests/baselines/local
135135

136136
.nx/cache
137137
.nx/workspace-data
138+
139+
.npmrc

package-lock.json

+401-108
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/auto-complete/package.json

+24-2
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,34 @@
1717
"require": "./dist/index.cjs"
1818
},
1919
"types": "dist/index.d.ts",
20-
"bin": "dist/bin/cli.js",
20+
"bin": "dist/cli/bin.js",
2121
"scripts": {
2222
"format": "prettier --config ../../.prettierrc -w .",
2323
"format:check": "prettier --config ../../.prettierrc -c .",
2424
"lint": "eslint src",
2525
"lint:fix": "eslint src --fix",
2626
"typecheck": "tsc -p tsconfig.json --noEmit",
27+
"test": "mocha",
28+
"coverage": "c8 npm test",
2729
"build": "tsup",
2830
"prepublishOnly": "npm run build"
2931
},
32+
"mocha": {
33+
"import": "tsx/esm",
34+
"spec": "tests/**/*.spec.ts"
35+
},
36+
"c8": {
37+
"reporter": [
38+
"text",
39+
"lcovonly"
40+
],
41+
"check-coverage": true,
42+
"skip-full": true
43+
},
3044
"tsup": {
3145
"entry": [
3246
"src/index.ts",
33-
"src/bin/cli.ts"
47+
"src/cli/bin.ts"
3448
],
3549
"format": [
3650
"esm",
@@ -42,13 +56,21 @@
4256
"clean": true
4357
},
4458
"devDependencies": {
59+
"@types/chai": "^5.0.1",
60+
"@types/mocha": "^10.0.10",
61+
"@types/sinon": "^17.0.4",
4562
"@typescript-eslint/eslint-plugin": "^8.2.0",
4663
"@typescript-eslint/parser": "^8.2.0",
64+
"c8": "^10.1.3",
65+
"chai": "^5.2.0",
4766
"eslint": "^8.57.0",
4867
"eslint-plugin-import": "^2.26.0",
4968
"eslint-plugin-prettier": "^5.0.0",
69+
"mocha": "^11.1.0",
5070
"prettier": "^3.0.0",
71+
"sinon": "^19.0.2",
5172
"tsup": "^6.7.0",
73+
"tsx": "^4.19.3",
5274
"typescript": "5.6.x"
5375
},
5476
"dependencies": {

packages/auto-complete/src/app.ts renamed to packages/auto-complete/src/cli/app.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
// Copyright 2024 Bloomberg Finance L.P.
22
// Distributed under the terms of the Apache 2.0 license.
33
import { buildApplication, buildRouteMap } from "@stricli/core";
4-
import { installCommand, uninstallCommand } from "./commands";
5-
import pkg from "../package.json";
4+
import pkg from "../../package.json";
5+
import { installAllCommand, installCommand, uninstallAllCommand, uninstallCommand } from "./commands/spec";
66

77
const root = buildRouteMap({
88
routes: {
99
install: installCommand,
10+
installAll: installAllCommand,
1011
uninstall: uninstallCommand,
12+
uninstallAll: uninstallAllCommand,
1113
},
1214
aliases: {
1315
i: "install",
16+
I: "installAll",
17+
u: "uninstall",
18+
U: "uninstallAll",
1419
},
1520
docs: {
1621
brief: "Manage auto-complete command installations for shells",
@@ -22,4 +27,7 @@ export const app = buildApplication(root, {
2227
versionInfo: {
2328
getCurrentVersion: async () => pkg.version,
2429
},
30+
scanner: {
31+
caseStyle: "allow-kebab-for-camel",
32+
},
2533
});

packages/auto-complete/src/bin/cli.ts renamed to packages/auto-complete/src/cli/bin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
// Copyright 2024 Bloomberg Finance L.P.
33
// Distributed under the terms of the Apache 2.0 license.
44
import { run } from "@stricli/core";
5-
import { app } from "../app";
5+
import { app } from "./app";
66
void run(app, process.argv.slice(2), globalThis);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2024 Bloomberg Finance L.P.
2+
// Distributed under the terms of the Apache 2.0 license.
3+
import shells from "../../shells";
4+
import type { Shell } from "../../types";
5+
import type { StricliAutoCompleteContext } from "../context";
6+
import type { ActiveShells, ShellAutoCompleteCommands } from "./spec";
7+
8+
export type InstallAllFlags = {
9+
readonly autocompleteCommand: string;
10+
readonly skipInactiveShells: boolean;
11+
};
12+
13+
export async function installAll(
14+
this: StricliAutoCompleteContext,
15+
flags: InstallAllFlags,
16+
targetCommand: string,
17+
): Promise<void> {
18+
for (const [shell, support] of Object.entries(shells)) {
19+
if (flags.skipInactiveShells && !(await support.isShellActive(this))) {
20+
this.process.stdout.write(`Skipping ${shell} as it is not active\n`);
21+
continue;
22+
}
23+
const shellManager = await support.getCommandManager(this);
24+
const message = await shellManager.install(targetCommand, `${flags.autocompleteCommand} ${shell}`);
25+
this.process.stdout.write(`Installed autocomplete support for ${targetCommand} in ${shell}\n`);
26+
if (message) {
27+
this.process.stdout.write(`${message}\n`);
28+
}
29+
}
30+
}
31+
32+
export type UninstallAllFlags = {
33+
readonly skipInactiveShells: boolean;
34+
};
35+
36+
export async function uninstallAll(
37+
this: StricliAutoCompleteContext,
38+
flags: UninstallAllFlags,
39+
targetCommand: string,
40+
): Promise<void> {
41+
for (const [shell, support] of Object.entries(shells)) {
42+
if (flags.skipInactiveShells && !(await support.isShellActive(this))) {
43+
this.process.stdout.write(`Skipping ${shell} as it is not active\n`);
44+
continue;
45+
}
46+
const shellManager = await support.getCommandManager(this);
47+
const message = await shellManager.uninstall(targetCommand);
48+
this.process.stdout.write(`Installed autocomplete support for ${targetCommand} in ${shell}\n`);
49+
if (message) {
50+
this.process.stdout.write(`${message}\n`);
51+
}
52+
}
53+
}
54+
55+
export async function install(
56+
this: StricliAutoCompleteContext,
57+
flags: ShellAutoCompleteCommands,
58+
targetCommand: string,
59+
): Promise<void> {
60+
for (const [shell, autocompleteCommand] of Object.entries(flags)) {
61+
const shellManager = await shells[shell as Shell].getCommandManager(this);
62+
const message = await shellManager.install(targetCommand, autocompleteCommand);
63+
this.process.stdout.write(`Installed autocomplete support for ${targetCommand} in ${shell}\n`);
64+
if (message) {
65+
this.process.stdout.write(`${message}\n`);
66+
}
67+
}
68+
}
69+
70+
export async function uninstall(
71+
this: StricliAutoCompleteContext,
72+
flags: ActiveShells,
73+
targetCommand: string,
74+
): Promise<void> {
75+
const activeShells = Object.entries(flags)
76+
.filter(([, active]) => active)
77+
.map(([shell]) => shell as Shell);
78+
for (const shell of activeShells) {
79+
const shellManager = await shells[shell].getCommandManager(this);
80+
const message = await shellManager.uninstall(targetCommand);
81+
this.process.stdout.write(`Uninstalled autocomplete support for ${targetCommand} in ${shell}\n`);
82+
if (message) {
83+
this.process.stdout.write(`${message}\n`);
84+
}
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Copyright 2024 Bloomberg Finance L.P.
2+
// Distributed under the terms of the Apache 2.0 license.
3+
import { buildCommand, type Command, type TypedPositionalParameter } from "@stricli/core";
4+
import { joinWithGrammar } from "../../util/formatting";
5+
import type { Shell } from "../../types";
6+
import type { StricliAutoCompleteContext } from "../context";
7+
import type { InstallAllFlags, UninstallAllFlags } from "./impl";
8+
9+
export type ShellAutoCompleteCommands = Readonly<Partial<Record<Shell, string>>>;
10+
11+
export type ActiveShells = Readonly<Partial<Record<Shell, boolean>>>;
12+
13+
const targetCommandParameter: TypedPositionalParameter<string> = {
14+
parse: String,
15+
brief: "Target command run by user, typically the application name",
16+
placeholder: "targetCommand",
17+
};
18+
19+
export const installAllCommand = buildCommand({
20+
loader: async () => {
21+
const { installAll } = await import("./impl");
22+
return installAll;
23+
},
24+
parameters: {
25+
flags: {
26+
autocompleteCommand: {
27+
kind: "parsed",
28+
brief: "Command to generate completion proposals, first argument is shell type",
29+
parse: String,
30+
placeholder: "command",
31+
},
32+
skipInactiveShells: {
33+
kind: "boolean",
34+
brief: "Skip installing for inactive shell(s)",
35+
default: true,
36+
},
37+
},
38+
positional: {
39+
kind: "tuple",
40+
parameters: [targetCommandParameter],
41+
},
42+
},
43+
docs: {
44+
brief: "Installs autocomplete support for target command on all shell types",
45+
},
46+
});
47+
48+
export function buildInstallAllCommand<CONTEXT extends StricliAutoCompleteContext>(
49+
targetCommand: string,
50+
flags: InstallAllFlags,
51+
): Command<CONTEXT> {
52+
return buildCommand({
53+
loader: async () => {
54+
const { installAll } = await import("./impl");
55+
return function () {
56+
return installAll.call(this, flags, targetCommand);
57+
};
58+
},
59+
parameters: {},
60+
docs: {
61+
brief: `Installs autocomplete support for ${targetCommand} on all shell types`,
62+
},
63+
});
64+
}
65+
66+
export const uninstallAllCommand = buildCommand({
67+
loader: async () => {
68+
const { uninstallAll } = await import("./impl");
69+
return uninstallAll;
70+
},
71+
parameters: {
72+
flags: {
73+
skipInactiveShells: {
74+
kind: "boolean",
75+
brief: "Skip installing for inactive shell(s)",
76+
default: true,
77+
},
78+
},
79+
positional: {
80+
kind: "tuple",
81+
parameters: [targetCommandParameter],
82+
},
83+
},
84+
docs: {
85+
brief: "Uninstalls autocomplete support for target command on all shell types",
86+
},
87+
});
88+
89+
export function buildUninstallAllCommand<CONTEXT extends StricliAutoCompleteContext>(
90+
targetCommand: string,
91+
flags: UninstallAllFlags,
92+
): Command<CONTEXT> {
93+
return buildCommand({
94+
loader: async () => {
95+
const { uninstallAll } = await import("./impl");
96+
return function () {
97+
return uninstallAll.call(this, flags, targetCommand);
98+
};
99+
},
100+
parameters: {},
101+
docs: {
102+
brief: `Uninstalls autocomplete support for ${targetCommand} on all shell types`,
103+
},
104+
});
105+
}
106+
107+
export const installCommand = buildCommand({
108+
loader: async () => {
109+
const { install } = await import("./impl");
110+
return install;
111+
},
112+
parameters: {
113+
flags: {
114+
bash: {
115+
kind: "parsed",
116+
brief: "Command executed by bash to generate completion proposals",
117+
parse: String,
118+
optional: true,
119+
placeholder: "command",
120+
},
121+
},
122+
positional: {
123+
kind: "tuple",
124+
parameters: [targetCommandParameter],
125+
},
126+
},
127+
docs: {
128+
brief: "Installs autocomplete support for target command on all provided shell types",
129+
},
130+
});
131+
132+
export function buildInstallCommand<CONTEXT extends StricliAutoCompleteContext>(
133+
targetCommand: string,
134+
commands: ShellAutoCompleteCommands,
135+
): Command<CONTEXT> {
136+
const shellsText = joinWithGrammar(Object.keys(commands), { conjunction: "and", serialComma: true });
137+
return buildCommand({
138+
loader: async () => {
139+
const { install } = await import("./impl");
140+
return function () {
141+
return install.call(this, commands, targetCommand);
142+
};
143+
},
144+
parameters: {},
145+
docs: {
146+
brief: `Installs ${shellsText} autocomplete support for ${targetCommand}`,
147+
},
148+
});
149+
}
150+
151+
export const uninstallCommand = buildCommand({
152+
loader: async () => {
153+
const { uninstall } = await import("./impl");
154+
return uninstall;
155+
},
156+
parameters: {
157+
flags: {
158+
bash: {
159+
kind: "boolean",
160+
brief: "Uninstall autocompletion for bash",
161+
optional: true,
162+
},
163+
},
164+
positional: {
165+
kind: "tuple",
166+
parameters: [targetCommandParameter],
167+
},
168+
},
169+
docs: {
170+
brief: "Uninstalls autocomplete support for target command on all selected shell types",
171+
},
172+
});
173+
174+
export function buildUninstallCommand<CONTEXT extends StricliAutoCompleteContext>(
175+
targetCommand: string,
176+
shells: ActiveShells,
177+
): Command<CONTEXT> {
178+
const activeShells = Object.entries(shells)
179+
.filter(([, enabled]) => enabled)
180+
.map(([shell]) => shell);
181+
const shellsText = joinWithGrammar(activeShells, { conjunction: "and", serialComma: true });
182+
return buildCommand({
183+
loader: async () => {
184+
const { uninstall } = await import("./impl");
185+
return function () {
186+
return uninstall.call(this, shells, targetCommand);
187+
};
188+
},
189+
parameters: {},
190+
docs: {
191+
brief: `Uninstalls ${shellsText} autocomplete support for ${targetCommand}`,
192+
},
193+
});
194+
}

0 commit comments

Comments
 (0)