launchr-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
6
+ import { runCli } from "../src/cli.mjs";
7
+
8
+ function createMemoryStream() {
9
+ let buffer = "";
10
+ return {
11
+ write(chunk) {
12
+ buffer += String(chunk);
13
+ },
14
+ toString() {
15
+ return buffer;
16
+ },
17
+ };
18
+ }
19
+
20
+ function buildSampleConfig() {
21
+ return {
22
+ grafana: {
23
+ description: "some useful information",
24
+ url: "https://grafana.com/{environments}/{query}/{timeframe}",
25
+ parameters: {
26
+ environments: {
27
+ type: "single-choice-list",
28
+ flag: "e",
29
+ defaultValue: "staging",
30
+ required: true,
31
+ values: ["staging", "production"],
32
+ },
33
+ query: {
34
+ type: "string",
35
+ flag: "q",
36
+ defaultValue: "error",
37
+ required: true,
38
+ },
39
+ timeframe: {
40
+ type: "single-choice-list",
41
+ flag: "t",
42
+ defaultValue: "5m",
43
+ required: true,
44
+ values: ["5m", "10m", "1h", "6h"],
45
+ },
46
+ },
47
+ },
48
+ };
49
+ }
50
+
51
+ async function createHomeWithConfig(configObject) {
52
+ const homeDir = await mkdtemp(path.join(os.tmpdir(), "launchr-cli-test-"));
53
+ const configDir = path.join(homeDir, ".launchr-configurations");
54
+ const configFile = path.join(configDir, "launchr-commands.json");
55
+ await mkdir(configDir, { recursive: true });
56
+ await writeFile(configFile, `${JSON.stringify(configObject, null, 2)}\n`, "utf8");
57
+ return homeDir;
58
+ }
59
+
60
+ test("runCli list prints configured commands", async () => {
61
+ const homeDir = await createHomeWithConfig(buildSampleConfig());
62
+ const stdout = createMemoryStream();
63
+ const stderr = createMemoryStream();
64
+
65
+ const code = await runCli(["list"], { homeDir, stdout, stderr });
66
+ assert.equal(code, 0);
67
+ assert.match(stdout.toString(), /grafana\s+- some useful information/);
68
+ assert.equal(stderr.toString(), "");
69
+ });
70
+
71
+ test("runCli with no command prints custom commands with descriptions", async () => {
72
+ const config = {
73
+ ...buildSampleConfig(),
74
+ yutup: {
75
+ description: "youtube ac",
76
+ url: "https://youtube.com",
77
+ parameters: {},
78
+ },
79
+ };
80
+
81
+ const homeDir = await createHomeWithConfig(config);
82
+ const stdout = createMemoryStream();
83
+ const stderr = createMemoryStream();
84
+
85
+ const code = await runCli([], { homeDir, stdout, stderr });
86
+
87
+ assert.equal(code, 0);
88
+ assert.match(stdout.toString(), /Built-in Commands:/);
89
+ assert.match(stdout.toString(), /Custom Commands:/);
90
+ assert.match(stdout.toString(), /^\s{2}help\s{2,}Show manual$/m);
91
+ assert.match(stdout.toString(), /^\s{2}list\s{2,}List available commands$/m);
92
+ assert.match(stdout.toString(), /^\s{2}init\s{2,}Interactive setup$/m);
93
+ assert.match(stdout.toString(), /^\s{2}grafana\s{2,}some useful information$/m);
94
+ assert.match(stdout.toString(), /^\s{2}yutup\s{2,}youtube ac$/m);
95
+ assert.equal(stderr.toString(), "");
96
+ });
97
+
98
+ test("runCli custom command validates params and opens URL", async () => {
99
+ const homeDir = await createHomeWithConfig(buildSampleConfig());
100
+ const stdout = createMemoryStream();
101
+ const stderr = createMemoryStream();
102
+ const openedUrls = [];
103
+
104
+ const code = await runCli(
105
+ ["grafana", "-e", "production", "-q", "error", "-t", "5m"],
106
+ {
107
+ homeDir,
108
+ stdout,
109
+ stderr,
110
+ openUrlFn: async (url) => {
111
+ openedUrls.push(url);
112
+ },
113
+ },
114
+ );
115
+
116
+ assert.equal(code, 0);
117
+ assert.deepEqual(openedUrls, ["https://grafana.com/production/error/5m"]);
118
+ assert.match(stdout.toString(), /Opening URL:/);
119
+ assert.equal(stderr.toString(), "");
120
+ });
121
+
122
+ test("runCli command help prints dynamic options", async () => {
123
+ const homeDir = await createHomeWithConfig(buildSampleConfig());
124
+ const stdout = createMemoryStream();
125
+ const stderr = createMemoryStream();
126
+
127
+ const code = await runCli(["grafana", "help"], {
128
+ homeDir,
129
+ stdout,
130
+ stderr,
131
+ });
132
+
133
+ assert.equal(code, 0);
134
+ assert.match(stdout.toString(), /launchr grafana -e <environments> -q <query> -t <timeframe>/);
135
+ assert.equal(stderr.toString(), "");
136
+ });
137
+
138
+ test("runCli exits with clear error when config creation is declined", async () => {
139
+ const homeDir = await mkdtemp(path.join(os.tmpdir(), "launchr-cli-test-"));
140
+ const stdout = createMemoryStream();
141
+ const stderr = createMemoryStream();
142
+
143
+ const code = await runCli(["help"], {
144
+ homeDir,
145
+ stdout,
146
+ stderr,
147
+ promptYesNoFn: async () => false,
148
+ });
149
+
150
+ assert.equal(code, 1);
151
+ assert.match(stderr.toString(), /Configuration file is required to use this CLI/);
152
+ });
@@ -0,0 +1,18 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseShortFlags } from "../src/parsing/flags.mjs";
4
+
5
+ test("parseShortFlags parses key-value pairs", () => {
6
+ const parsed = parseShortFlags(["-e", "production", "-q", "error"]);
7
+ assert.equal(parsed.get("e"), "production");
8
+ assert.equal(parsed.get("q"), "error");
9
+ });
10
+
11
+ test("parseShortFlags treats missing value as boolean true", () => {
12
+ const parsed = parseShortFlags(["-d"]);
13
+ assert.equal(parsed.get("d"), true);
14
+ });
15
+
16
+ test("parseShortFlags rejects invalid token", () => {
17
+ assert.throws(() => parseShortFlags(["query"]), /Unexpected token/);
18
+ });
@@ -0,0 +1,52 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { runInitFlow } from "../src/commands/init.mjs";
4
+
5
+ function createScriptedPrompter(answers) {
6
+ const queue = [...answers];
7
+ const writes = [];
8
+ return {
9
+ async ask() {
10
+ if (queue.length === 0) {
11
+ throw new Error("No more scripted answers");
12
+ }
13
+ return queue.shift();
14
+ },
15
+ write(message) {
16
+ writes.push(message);
17
+ },
18
+ close() {},
19
+ getWrites() {
20
+ return writes;
21
+ },
22
+ };
23
+ }
24
+
25
+ test("runInitFlow collects command definitions and saves config", async () => {
26
+ const prompter = createScriptedPrompter([
27
+ "google",
28
+ "search utility",
29
+ "https://google.com?q={query}",
30
+ "query",
31
+ "string",
32
+ "q",
33
+ "true",
34
+ "",
35
+ "done",
36
+ "no",
37
+ ]);
38
+
39
+ let savedConfig;
40
+ const result = await runInitFlow({
41
+ config: {},
42
+ prompter,
43
+ saveConfig: async (config) => {
44
+ savedConfig = config;
45
+ },
46
+ });
47
+
48
+ assert.equal(result.google.description, "search utility");
49
+ assert.equal(result.google.url, "https://google.com?q={query}");
50
+ assert.equal(result.google.parameters.query.flag, "q");
51
+ assert.equal(savedConfig.google.parameters.query.required, true);
52
+ });
@@ -0,0 +1,63 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { resolveRuntimeParameters } from "../src/validation/runtime-params.mjs";
4
+
5
+ const commandConfig = {
6
+ description: "search",
7
+ url: "https://google.com?query={query}&limit={limit}&debug={debug}",
8
+ parameters: {
9
+ query: {
10
+ type: "string",
11
+ flag: "q",
12
+ required: true,
13
+ },
14
+ limit: {
15
+ type: "integer",
16
+ flag: "l",
17
+ required: false,
18
+ defaultValue: 10,
19
+ },
20
+ debug: {
21
+ type: "boolean",
22
+ flag: "d",
23
+ required: false,
24
+ defaultValue: false,
25
+ },
26
+ },
27
+ };
28
+
29
+ test("resolveRuntimeParameters validates required fields", () => {
30
+ const flags = new Map();
31
+ assert.throws(
32
+ () => resolveRuntimeParameters("google", commandConfig, flags),
33
+ /Missing required parameter/,
34
+ );
35
+ });
36
+
37
+ test("resolveRuntimeParameters uses defaults and normalizes types", () => {
38
+ const flags = new Map([["q", "error"]]);
39
+ const result = resolveRuntimeParameters("google", commandConfig, flags);
40
+ assert.deepEqual(result.valuesInOrder, ["error", 10, false]);
41
+ });
42
+
43
+ test("resolveRuntimeParameters rejects unknown flags", () => {
44
+ const flags = new Map([
45
+ ["q", "error"],
46
+ ["x", "1"],
47
+ ]);
48
+
49
+ assert.throws(
50
+ () => resolveRuntimeParameters("google", commandConfig, flags),
51
+ /Unknown flag/,
52
+ );
53
+ });
54
+
55
+ test("resolveRuntimeParameters parses boolean true flag without explicit value", () => {
56
+ const flags = new Map([
57
+ ["q", "error"],
58
+ ["d", true],
59
+ ]);
60
+
61
+ const result = resolveRuntimeParameters("google", commandConfig, flags);
62
+ assert.deepEqual(result.valuesInOrder, ["error", 10, true]);
63
+ });
@@ -0,0 +1,101 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { validateConfigSchema } from "../src/config/schema.mjs";
4
+
5
+ test("validateConfigSchema accepts valid config", () => {
6
+ const config = {
7
+ grafana: {
8
+ description: "some useful information",
9
+ url: "https://grafana.com/{environments}/{query}/{timeframe}",
10
+ parameters: {
11
+ environments: {
12
+ type: "single-choice-list",
13
+ flag: "e",
14
+ defaultValue: "as01-prd01",
15
+ required: true,
16
+ values: ["as01-prd01", "eu01-prd01"],
17
+ },
18
+ query: {
19
+ type: "string",
20
+ flag: "q",
21
+ defaultValue: "error",
22
+ required: true,
23
+ },
24
+ timeframe: {
25
+ type: "single-choice-list",
26
+ flag: "t",
27
+ defaultValue: "5m",
28
+ required: true,
29
+ values: ["5m", "10m", "1h", "6h"],
30
+ },
31
+ },
32
+ },
33
+ };
34
+
35
+ assert.equal(validateConfigSchema(config), config);
36
+ });
37
+
38
+ test("validateConfigSchema rejects unknown placeholders", () => {
39
+ const config = {
40
+ google: {
41
+ description: "search",
42
+ url: "https://google.com/{query}/{scope}",
43
+ parameters: {
44
+ query: {
45
+ type: "string",
46
+ flag: "q",
47
+ required: true,
48
+ },
49
+ },
50
+ },
51
+ };
52
+
53
+ assert.throws(() => validateConfigSchema(config), /unknown placeholders/);
54
+ });
55
+
56
+ test("validateConfigSchema rejects missing placeholders", () => {
57
+ const config = {
58
+ google: {
59
+ description: "search",
60
+ url: "https://google.com/{query}",
61
+ parameters: {
62
+ query: {
63
+ type: "string",
64
+ flag: "q",
65
+ required: true,
66
+ },
67
+ scope: {
68
+ type: "string",
69
+ flag: "s",
70
+ required: false,
71
+ defaultValue: "all",
72
+ },
73
+ },
74
+ },
75
+ };
76
+
77
+ assert.throws(() => validateConfigSchema(config), /missing placeholders/);
78
+ });
79
+
80
+ test("validateConfigSchema rejects duplicated flags", () => {
81
+ const config = {
82
+ google: {
83
+ description: "search",
84
+ url: "https://google.com/{query}/{term}",
85
+ parameters: {
86
+ query: {
87
+ type: "string",
88
+ flag: "q",
89
+ required: true,
90
+ },
91
+ term: {
92
+ type: "string",
93
+ flag: "q",
94
+ required: true,
95
+ },
96
+ },
97
+ },
98
+ };
99
+
100
+ assert.throws(() => validateConfigSchema(config), /duplicated flag/);
101
+ });
@@ -0,0 +1,30 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { interpolateUrlTemplate, extractTemplatePlaceholders } from "../src/utils/url-template.mjs";
4
+
5
+ test("extractTemplatePlaceholders returns parameter keys in order", () => {
6
+ assert.deepEqual(
7
+ extractTemplatePlaceholders("https://grafana.com/{environments}/{query}/{timeframe}"),
8
+ ["environments", "query", "timeframe"],
9
+ );
10
+ });
11
+
12
+ test("interpolateUrlTemplate replaces named placeholders by key", () => {
13
+ const value = interpolateUrlTemplate(
14
+ "https://grafana.com/{environments}/{query}/{timeframe}",
15
+ {
16
+ environments: "staging",
17
+ query: "error signal",
18
+ timeframe: "5m",
19
+ },
20
+ );
21
+
22
+ assert.equal(value, "https://grafana.com/staging/error%20signal/5m");
23
+ });
24
+
25
+ test("interpolateUrlTemplate throws when placeholder has no matching value", () => {
26
+ assert.throws(
27
+ () => interpolateUrlTemplate("https://google.com/{query}/{scope}", { query: "k8s" }),
28
+ /no matching parameter value/,
29
+ );
30
+ });