@stackweld/cli 0.2.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.
Files changed (81) hide show
  1. package/dist/__tests__/commands.test.d.ts +2 -0
  2. package/dist/__tests__/commands.test.js +275 -0
  3. package/dist/commands/ai.d.ts +8 -0
  4. package/dist/commands/ai.js +167 -0
  5. package/dist/commands/analyze.d.ts +6 -0
  6. package/dist/commands/analyze.js +90 -0
  7. package/dist/commands/benchmark.d.ts +6 -0
  8. package/dist/commands/benchmark.js +86 -0
  9. package/dist/commands/browse.d.ts +6 -0
  10. package/dist/commands/browse.js +101 -0
  11. package/dist/commands/clone.d.ts +3 -0
  12. package/dist/commands/clone.js +37 -0
  13. package/dist/commands/compare.d.ts +6 -0
  14. package/dist/commands/compare.js +93 -0
  15. package/dist/commands/completion.d.ts +6 -0
  16. package/dist/commands/completion.js +86 -0
  17. package/dist/commands/config.d.ts +6 -0
  18. package/dist/commands/config.js +56 -0
  19. package/dist/commands/cost.d.ts +6 -0
  20. package/dist/commands/cost.js +101 -0
  21. package/dist/commands/create.d.ts +7 -0
  22. package/dist/commands/create.js +111 -0
  23. package/dist/commands/delete.d.ts +6 -0
  24. package/dist/commands/delete.js +33 -0
  25. package/dist/commands/deploy.d.ts +6 -0
  26. package/dist/commands/deploy.js +90 -0
  27. package/dist/commands/doctor.d.ts +6 -0
  28. package/dist/commands/doctor.js +144 -0
  29. package/dist/commands/down.d.ts +6 -0
  30. package/dist/commands/down.js +37 -0
  31. package/dist/commands/env.d.ts +6 -0
  32. package/dist/commands/env.js +129 -0
  33. package/dist/commands/export-stack.d.ts +6 -0
  34. package/dist/commands/export-stack.js +51 -0
  35. package/dist/commands/generate.d.ts +9 -0
  36. package/dist/commands/generate.js +542 -0
  37. package/dist/commands/health.d.ts +6 -0
  38. package/dist/commands/health.js +68 -0
  39. package/dist/commands/import-stack.d.ts +6 -0
  40. package/dist/commands/import-stack.js +68 -0
  41. package/dist/commands/info.d.ts +6 -0
  42. package/dist/commands/info.js +56 -0
  43. package/dist/commands/init.d.ts +6 -0
  44. package/dist/commands/init.js +186 -0
  45. package/dist/commands/learn.d.ts +6 -0
  46. package/dist/commands/learn.js +91 -0
  47. package/dist/commands/lint.d.ts +6 -0
  48. package/dist/commands/lint.js +193 -0
  49. package/dist/commands/list.d.ts +6 -0
  50. package/dist/commands/list.js +27 -0
  51. package/dist/commands/logs.d.ts +6 -0
  52. package/dist/commands/logs.js +37 -0
  53. package/dist/commands/migrate.d.ts +6 -0
  54. package/dist/commands/migrate.js +57 -0
  55. package/dist/commands/plugin.d.ts +8 -0
  56. package/dist/commands/plugin.js +131 -0
  57. package/dist/commands/preview.d.ts +7 -0
  58. package/dist/commands/preview.js +100 -0
  59. package/dist/commands/save.d.ts +6 -0
  60. package/dist/commands/save.js +32 -0
  61. package/dist/commands/scaffold.d.ts +7 -0
  62. package/dist/commands/scaffold.js +100 -0
  63. package/dist/commands/score.d.ts +9 -0
  64. package/dist/commands/score.js +111 -0
  65. package/dist/commands/share.d.ts +10 -0
  66. package/dist/commands/share.js +93 -0
  67. package/dist/commands/status.d.ts +6 -0
  68. package/dist/commands/status.js +39 -0
  69. package/dist/commands/template.d.ts +3 -0
  70. package/dist/commands/template.js +162 -0
  71. package/dist/commands/up.d.ts +6 -0
  72. package/dist/commands/up.js +54 -0
  73. package/dist/commands/version-cmd.d.ts +6 -0
  74. package/dist/commands/version-cmd.js +100 -0
  75. package/dist/index.d.ts +6 -0
  76. package/dist/index.js +160 -0
  77. package/dist/ui/context.d.ts +10 -0
  78. package/dist/ui/context.js +90 -0
  79. package/dist/ui/format.d.ts +59 -0
  80. package/dist/ui/format.js +295 -0
  81. package/package.json +52 -0
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=commands.test.d.ts.map
@@ -0,0 +1,275 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { banner, box, emptyState, error, info, progressBar, stepIndicator, success, table, warning, } from "../ui/format.js";
3
+ // Helper to strip ANSI escape codes for content assertions
4
+ function stripAnsi(str) {
5
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
6
+ }
7
+ // ─── format.ts utility functions ─────────────────────────
8
+ describe("format.ts utility functions", () => {
9
+ describe("success()", () => {
10
+ it("returns string containing green checkmark", () => {
11
+ const result = success("done");
12
+ const plain = stripAnsi(result);
13
+ expect(plain).toContain("\u2714");
14
+ expect(plain).toContain("done");
15
+ });
16
+ it("includes the message text", () => {
17
+ const result = success("All tests passed");
18
+ expect(stripAnsi(result)).toContain("All tests passed");
19
+ });
20
+ });
21
+ describe("error()", () => {
22
+ it("returns string containing red X", () => {
23
+ const result = error("failed");
24
+ const plain = stripAnsi(result);
25
+ expect(plain).toContain("\u2716");
26
+ expect(plain).toContain("failed");
27
+ });
28
+ it("includes the message text", () => {
29
+ const result = error("Build failed");
30
+ expect(stripAnsi(result)).toContain("Build failed");
31
+ });
32
+ });
33
+ describe("warning()", () => {
34
+ it("returns string containing warning triangle", () => {
35
+ const result = warning("careful");
36
+ const plain = stripAnsi(result);
37
+ expect(plain).toContain("\u26A0");
38
+ expect(plain).toContain("careful");
39
+ });
40
+ });
41
+ describe("info()", () => {
42
+ it("returns string containing info symbol", () => {
43
+ const result = info("note");
44
+ const plain = stripAnsi(result);
45
+ expect(plain).toContain("\u2139");
46
+ expect(plain).toContain("note");
47
+ });
48
+ });
49
+ describe("banner()", () => {
50
+ it("returns formatted banner with version", () => {
51
+ const result = banner("1.0.0");
52
+ const plain = stripAnsi(result);
53
+ expect(plain).toContain("Stackweld");
54
+ expect(plain).toContain("v1.0.0");
55
+ });
56
+ it("includes tagline", () => {
57
+ const result = banner("2.0.0");
58
+ const plain = stripAnsi(result);
59
+ expect(plain).toContain("operating system for your dev stacks");
60
+ });
61
+ it("returns multi-line output", () => {
62
+ const result = banner("1.0.0");
63
+ const lines = result.split("\n");
64
+ expect(lines.length).toBeGreaterThan(1);
65
+ });
66
+ });
67
+ describe("box()", () => {
68
+ it("draws Unicode box around content", () => {
69
+ const result = box("hello");
70
+ expect(result).toContain("\u256D"); // top-left corner
71
+ expect(result).toContain("\u256E"); // top-right corner
72
+ expect(result).toContain("\u2570"); // bottom-left corner
73
+ expect(result).toContain("\u256F"); // bottom-right corner
74
+ expect(result).toContain("\u2502"); // vertical bar
75
+ expect(result).toContain("\u2500"); // horizontal bar
76
+ });
77
+ it("includes the content inside the box", () => {
78
+ const result = box("test content");
79
+ const plain = stripAnsi(result);
80
+ expect(plain).toContain("test content");
81
+ });
82
+ it("supports an optional title", () => {
83
+ const result = box("body", "Title");
84
+ const plain = stripAnsi(result);
85
+ expect(plain).toContain("Title");
86
+ expect(plain).toContain("body");
87
+ });
88
+ it("handles multi-line content", () => {
89
+ const result = box("line1\nline2\nline3");
90
+ const plain = stripAnsi(result);
91
+ expect(plain).toContain("line1");
92
+ expect(plain).toContain("line2");
93
+ expect(plain).toContain("line3");
94
+ });
95
+ });
96
+ describe("table()", () => {
97
+ it("formats data in aligned columns", () => {
98
+ const data = [
99
+ { name: "Next.js", category: "frontend" },
100
+ { name: "PostgreSQL", category: "database" },
101
+ ];
102
+ const columns = [
103
+ { header: "Name", key: "name" },
104
+ { header: "Category", key: "category" },
105
+ ];
106
+ const result = table(data, columns);
107
+ const plain = stripAnsi(result);
108
+ expect(plain).toContain("Name");
109
+ expect(plain).toContain("Category");
110
+ expect(plain).toContain("Next.js");
111
+ expect(plain).toContain("PostgreSQL");
112
+ expect(plain).toContain("frontend");
113
+ expect(plain).toContain("database");
114
+ });
115
+ it("returns empty string for empty data", () => {
116
+ const result = table([], [{ header: "Name", key: "name" }]);
117
+ expect(result).toBe("");
118
+ });
119
+ it("includes a separator line between header and data", () => {
120
+ const data = [{ name: "test" }];
121
+ const columns = [{ header: "Name", key: "name" }];
122
+ const result = table(data, columns);
123
+ const plain = stripAnsi(result);
124
+ const lines = plain.split("\n");
125
+ // line 0 = header, line 1 = separator, line 2+ = data
126
+ expect(lines.length).toBeGreaterThanOrEqual(3);
127
+ expect(lines[1]).toMatch(/\u2500+/);
128
+ });
129
+ it("handles missing keys gracefully", () => {
130
+ const data = [{ name: "test" }];
131
+ const columns = [
132
+ { header: "Name", key: "name" },
133
+ { header: "Missing", key: "nonexistent" },
134
+ ];
135
+ const result = table(data, columns);
136
+ // Should not throw, just produce empty value
137
+ expect(stripAnsi(result)).toContain("Name");
138
+ });
139
+ });
140
+ describe("stepIndicator()", () => {
141
+ it("shows correct step count", () => {
142
+ const result = stepIndicator(2, 5, "Processing");
143
+ const plain = stripAnsi(result);
144
+ expect(plain).toContain("[2/5]");
145
+ expect(plain).toContain("Processing");
146
+ });
147
+ it("works with step 1 of 1", () => {
148
+ const result = stepIndicator(1, 1, "Only step");
149
+ const plain = stripAnsi(result);
150
+ expect(plain).toContain("[1/1]");
151
+ expect(plain).toContain("Only step");
152
+ });
153
+ });
154
+ describe("emptyState()", () => {
155
+ it("returns formatted empty message", () => {
156
+ const result = emptyState("No items found");
157
+ const plain = stripAnsi(result);
158
+ expect(plain).toContain("No items found");
159
+ });
160
+ it("includes hint when provided", () => {
161
+ const result = emptyState("No stacks", "Run stackweld create to get started");
162
+ const plain = stripAnsi(result);
163
+ expect(plain).toContain("No stacks");
164
+ expect(plain).toContain("Run stackweld create to get started");
165
+ });
166
+ it("works without hint", () => {
167
+ const result = emptyState("Empty");
168
+ const plain = stripAnsi(result);
169
+ expect(plain).toContain("Empty");
170
+ // Should not throw
171
+ });
172
+ });
173
+ describe("progressBar()", () => {
174
+ it("shows 0% for zero progress", () => {
175
+ const result = progressBar(0, 10);
176
+ const plain = stripAnsi(result);
177
+ expect(plain).toContain("0%");
178
+ });
179
+ it("shows 50% for half progress", () => {
180
+ const result = progressBar(5, 10);
181
+ const plain = stripAnsi(result);
182
+ expect(plain).toContain("50%");
183
+ });
184
+ it("shows 100% for complete progress", () => {
185
+ const result = progressBar(10, 10);
186
+ const plain = stripAnsi(result);
187
+ expect(plain).toContain("100%");
188
+ });
189
+ it("caps at 100% when current exceeds total", () => {
190
+ const result = progressBar(15, 10);
191
+ const plain = stripAnsi(result);
192
+ expect(plain).toContain("100%");
193
+ });
194
+ it("contains filled and empty bar characters", () => {
195
+ const result = progressBar(5, 10, 20);
196
+ // Should contain both filled (\u2588) and empty (\u2591) blocks
197
+ expect(result).toContain("\u2588");
198
+ expect(result).toContain("\u2591");
199
+ });
200
+ it("respects custom width", () => {
201
+ const result = progressBar(5, 10, 40);
202
+ // The bar portion should contain 40 total bar characters
203
+ const filled = (result.match(/\u2588/g) || []).length;
204
+ const empty = (result.match(/\u2591/g) || []).length;
205
+ expect(filled + empty).toBe(40);
206
+ });
207
+ });
208
+ });
209
+ // ─── Input validation (from generate.ts) ────────────────
210
+ import * as nodePath from "node:path";
211
+ describe("Input validation for project names", () => {
212
+ // Replicate the validation logic from generate.ts (lines 426-429):
213
+ // const safeName = path.basename(opts.name);
214
+ // if (safeName !== opts.name || !/^[a-zA-Z0-9_.\-]+$/.test(safeName))
215
+ const path = nodePath;
216
+ function isValidProjectName(name) {
217
+ const safeName = path.basename(name);
218
+ return safeName === name && /^[a-zA-Z0-9_.-]+$/.test(safeName);
219
+ }
220
+ describe("rejects path traversal attempts", () => {
221
+ it("rejects ../evil", () => {
222
+ expect(isValidProjectName("../evil")).toBe(false);
223
+ });
224
+ it("rejects ../../etc/passwd", () => {
225
+ expect(isValidProjectName("../../etc/passwd")).toBe(false);
226
+ });
227
+ it("rejects subdir/project", () => {
228
+ expect(isValidProjectName("subdir/project")).toBe(false);
229
+ });
230
+ });
231
+ describe("rejects shell injection characters", () => {
232
+ it("rejects names with semicolons", () => {
233
+ expect(isValidProjectName("project;rm -rf")).toBe(false);
234
+ });
235
+ it("rejects names with pipes", () => {
236
+ expect(isValidProjectName("project|evil")).toBe(false);
237
+ });
238
+ it("rejects names with dollar signs", () => {
239
+ expect(isValidProjectName("project$HOME")).toBe(false);
240
+ });
241
+ it("rejects names with backticks", () => {
242
+ expect(isValidProjectName("project`whoami`")).toBe(false);
243
+ });
244
+ it("rejects names with spaces", () => {
245
+ expect(isValidProjectName("my project")).toBe(false);
246
+ });
247
+ it("rejects empty string", () => {
248
+ expect(isValidProjectName("")).toBe(false);
249
+ });
250
+ });
251
+ describe("accepts valid project names", () => {
252
+ it("accepts my-project", () => {
253
+ expect(isValidProjectName("my-project")).toBe(true);
254
+ });
255
+ it("accepts test_app", () => {
256
+ expect(isValidProjectName("test_app")).toBe(true);
257
+ });
258
+ it("accepts app1", () => {
259
+ expect(isValidProjectName("app1")).toBe(true);
260
+ });
261
+ it("accepts names with dots", () => {
262
+ expect(isValidProjectName("my.app")).toBe(true);
263
+ });
264
+ it("accepts uppercase letters", () => {
265
+ expect(isValidProjectName("MyProject")).toBe(true);
266
+ });
267
+ it("accepts single character", () => {
268
+ expect(isValidProjectName("a")).toBe(true);
269
+ });
270
+ it("accepts numbers only", () => {
271
+ expect(isValidProjectName("123")).toBe(true);
272
+ });
273
+ });
274
+ });
275
+ //# sourceMappingURL=commands.test.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * stackweld ai — AI-powered utilities.
3
+ * These are utility features, NOT the source of truth for compatibility.
4
+ * Uses the Anthropic API for natural language processing.
5
+ */
6
+ import { Command } from "commander";
7
+ export declare const aiCommand: Command;
8
+ //# sourceMappingURL=ai.d.ts.map
@@ -0,0 +1,167 @@
1
+ /**
2
+ * stackweld ai — AI-powered utilities.
3
+ * These are utility features, NOT the source of truth for compatibility.
4
+ * Uses the Anthropic API for natural language processing.
5
+ */
6
+ import Anthropic from "@anthropic-ai/sdk";
7
+ import chalk from "chalk";
8
+ import { Command } from "commander";
9
+ import ora from "ora";
10
+ import { getRulesEngine, getStackEngine } from "../ui/context.js";
11
+ import { box, error } from "../ui/format.js";
12
+ function getClient() {
13
+ const apiKey = process.env.ANTHROPIC_API_KEY;
14
+ if (!apiKey)
15
+ return null;
16
+ return new Anthropic({ apiKey });
17
+ }
18
+ function requireApiKey() {
19
+ const client = getClient();
20
+ if (!client) {
21
+ console.error("");
22
+ console.error(error("ANTHROPIC_API_KEY is not set."));
23
+ console.error("");
24
+ console.error(chalk.dim(" To use AI features, set your API key:"));
25
+ console.error(chalk.dim(" export ANTHROPIC_API_KEY=sk-ant-..."));
26
+ console.error("");
27
+ console.error(chalk.dim(" Get one at: https://console.anthropic.com/settings/keys"));
28
+ console.error("");
29
+ process.exit(1);
30
+ }
31
+ return client;
32
+ }
33
+ async function ask(client, systemPrompt, userPrompt) {
34
+ const response = await client.messages.create({
35
+ model: "claude-sonnet-4-20250514",
36
+ max_tokens: 1024,
37
+ system: systemPrompt,
38
+ messages: [{ role: "user", content: userPrompt }],
39
+ });
40
+ const block = response.content[0];
41
+ return block.type === "text" ? block.text : "";
42
+ }
43
+ export const aiCommand = new Command("ai")
44
+ .description("AI-powered utilities (requires ANTHROPIC_API_KEY)")
45
+ .addCommand(new Command("suggest")
46
+ .description("Suggest a stack from a natural language description")
47
+ .argument("<description>", "What you want to build")
48
+ .action(async (description) => {
49
+ const client = requireApiKey();
50
+ const rules = getRulesEngine();
51
+ const allTechs = rules.getAllTechnologies();
52
+ const techList = allTechs.map((t) => `${t.id} (${t.name}, ${t.category})`).join("\n");
53
+ const spinner = ora("Thinking...").start();
54
+ try {
55
+ const result = await ask(client, `You are Stackweld AI. You suggest technology stacks from a catalog.
56
+ Available technologies (use ONLY these IDs):
57
+ ${techList}
58
+
59
+ Respond with:
60
+ 1. A stack name
61
+ 2. Profile (rapid/standard/production/enterprise/lightweight)
62
+ 3. Technology IDs as a comma-separated list
63
+ 4. Brief explanation of why each technology was chosen
64
+
65
+ Be concise. Only suggest technologies from the list above.`, `I want to build: ${description}`);
66
+ spinner.stop();
67
+ console.log("");
68
+ console.log(box(result, "Suggested Stack"));
69
+ console.log("");
70
+ console.log(chalk.dim(" To create this stack: stackweld generate --name <name> --path . --techs <ids>"));
71
+ console.log("");
72
+ }
73
+ catch (err) {
74
+ spinner.fail("AI request failed");
75
+ if (err instanceof Error) {
76
+ if (err.message.includes("401") || err.message.includes("authentication")) {
77
+ console.error(error("Invalid API key. Check your ANTHROPIC_API_KEY."));
78
+ }
79
+ else if (err.message.includes("429")) {
80
+ console.error(error("Rate limited. Try again in a moment."));
81
+ }
82
+ else {
83
+ console.error(chalk.dim(` ${err.message}`));
84
+ }
85
+ }
86
+ }
87
+ }))
88
+ .addCommand(new Command("readme")
89
+ .description("Generate a README from a saved stack")
90
+ .argument("<stack-id>", "Stack ID")
91
+ .action(async (stackId) => {
92
+ const client = requireApiKey();
93
+ const engine = getStackEngine();
94
+ const rules = getRulesEngine();
95
+ const stack = engine.get(stackId);
96
+ if (!stack) {
97
+ console.error(error(`Stack "${stackId}" not found.`));
98
+ console.error(chalk.dim(" Run `stackweld list` to see saved stacks."));
99
+ process.exit(1);
100
+ }
101
+ const techDetails = stack.technologies
102
+ .map((st) => {
103
+ const tech = rules.getTechnology(st.technologyId);
104
+ return tech
105
+ ? `- ${tech.name} v${st.version} (${tech.category}): ${tech.description}`
106
+ : `- ${st.technologyId} v${st.version}`;
107
+ })
108
+ .join("\n");
109
+ const spinner = ora("Generating README...").start();
110
+ try {
111
+ const result = await ask(client, `You generate professional README.md files for software projects. Write clean, practical Markdown. No emojis.`, `Generate a README.md for a project called "${stack.name}" (${stack.description || "no description"}).
112
+ Profile: ${stack.profile}
113
+ Technologies:
114
+ ${techDetails}
115
+
116
+ Include: project description, tech stack table, getting started (with docker compose up, env setup), development commands, and project structure suggestions.`);
117
+ spinner.stop();
118
+ console.log(result);
119
+ }
120
+ catch (err) {
121
+ spinner.fail("AI request failed");
122
+ console.error(chalk.dim(err instanceof Error ? err.message : String(err)));
123
+ }
124
+ }))
125
+ .addCommand(new Command("explain")
126
+ .description("Explain the decisions in a stack")
127
+ .argument("<stack-id>", "Stack ID")
128
+ .action(async (stackId) => {
129
+ const client = requireApiKey();
130
+ const engine = getStackEngine();
131
+ const rules = getRulesEngine();
132
+ const stack = engine.get(stackId);
133
+ if (!stack) {
134
+ console.error(error(`Stack "${stackId}" not found.`));
135
+ console.error(chalk.dim(" Run `stackweld list` to see saved stacks."));
136
+ process.exit(1);
137
+ }
138
+ const techDetails = stack.technologies
139
+ .map((st) => {
140
+ const tech = rules.getTechnology(st.technologyId);
141
+ return tech ? `${tech.name} (${tech.category})` : st.technologyId;
142
+ })
143
+ .join(", ");
144
+ const spinner = ora("Analyzing stack...").start();
145
+ try {
146
+ const result = await ask(client, `You are a senior software architect. Explain technology choices concisely. No emojis.`, `Explain the architecture decisions in this stack:
147
+ Name: ${stack.name}
148
+ Profile: ${stack.profile}
149
+ Technologies: ${techDetails}
150
+
151
+ For each technology, explain:
152
+ 1. Why it was likely chosen
153
+ 2. How it fits with the other pieces
154
+ 3. Any trade-offs to be aware of
155
+
156
+ Be concise and practical.`);
157
+ spinner.stop();
158
+ console.log("");
159
+ console.log(box(result, "Stack Analysis"));
160
+ console.log("");
161
+ }
162
+ catch (err) {
163
+ spinner.fail("AI request failed");
164
+ console.error(chalk.dim(err instanceof Error ? err.message : String(err)));
165
+ }
166
+ }));
167
+ //# sourceMappingURL=ai.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * stackweld analyze [path] — Detect the technology stack of a project.
3
+ */
4
+ import { Command } from "commander";
5
+ export declare const analyzeCommand: Command;
6
+ //# sourceMappingURL=analyze.d.ts.map
@@ -0,0 +1,90 @@
1
+ /**
2
+ * stackweld analyze [path] — Detect the technology stack of a project.
3
+ */
4
+ import * as path from "node:path";
5
+ import { detectStack } from "@stackweld/core";
6
+ import chalk from "chalk";
7
+ import { Command } from "commander";
8
+ import { getStackEngine } from "../ui/context.js";
9
+ import { box, error, formatJson, success } from "../ui/format.js";
10
+ const PROJECT_TYPE_LABELS = {
11
+ frontend: "Frontend",
12
+ backend: "Backend",
13
+ fullstack: "Full-Stack",
14
+ monorepo: "Monorepo",
15
+ library: "Library",
16
+ unknown: "Unknown",
17
+ };
18
+ export const analyzeCommand = new Command("analyze")
19
+ .description("Detect the technology stack of a project")
20
+ .argument("[path]", "Project directory to analyze", process.cwd())
21
+ .option("--import", "Save detected stack to the database")
22
+ .option("--json", "Output as JSON")
23
+ .action((targetPath, opts) => {
24
+ const projectDir = path.resolve(targetPath);
25
+ let result;
26
+ try {
27
+ result = detectStack(projectDir);
28
+ }
29
+ catch (err) {
30
+ console.error(error(`Failed to analyze project: ${err instanceof Error ? err.message : String(err)}`));
31
+ process.exit(1);
32
+ }
33
+ if (opts.json) {
34
+ console.log(formatJson(result));
35
+ return;
36
+ }
37
+ const lines = [];
38
+ // Project type
39
+ const typeLabel = PROJECT_TYPE_LABELS[result.projectType] || result.projectType;
40
+ lines.push(`${chalk.dim("Project Type:")} ${chalk.bold(typeLabel)} ${chalk.dim(`(${result.confidence}%)`)}`);
41
+ lines.push("");
42
+ // Technologies
43
+ if (result.technologies.length > 0) {
44
+ lines.push(chalk.bold("Detected Technologies:"));
45
+ for (const tech of result.technologies) {
46
+ const version = tech.version ? ` ${tech.version}` : "";
47
+ const via = chalk.dim(`(${tech.detectedVia})`);
48
+ lines.push(`${chalk.green("\u2714")} ${chalk.cyan(tech.name)}${chalk.dim(version)} ${via}`);
49
+ }
50
+ }
51
+ else {
52
+ lines.push(chalk.dim("No technologies detected."));
53
+ }
54
+ lines.push("");
55
+ // Package managers
56
+ if (result.packageManagers.length > 0) {
57
+ lines.push(`${chalk.dim("Package Managers:")} ${result.packageManagers.join(", ")}`);
58
+ lines.push("");
59
+ }
60
+ // Summary
61
+ const count = result.technologies.length;
62
+ lines.push(`${count} ${count === 1 ? "technology" : "technologies"} ${chalk.dim("\u2022")} Confidence: ${result.confidence}%`);
63
+ if (!opts.import) {
64
+ lines.push("");
65
+ lines.push(chalk.dim("Run with --import to save as a stack"));
66
+ }
67
+ console.log(box(lines.join("\n"), `Stack Detection: ${projectDir}`));
68
+ // Import mode
69
+ if (opts.import) {
70
+ try {
71
+ const engine = getStackEngine();
72
+ const stackTechs = result.technologies.map((t) => ({
73
+ technologyId: t.id,
74
+ version: t.version || "latest",
75
+ }));
76
+ const dirName = path.basename(projectDir);
77
+ const { stack } = engine.create({
78
+ name: dirName,
79
+ profile: "standard",
80
+ technologies: stackTechs,
81
+ });
82
+ console.log(success(`Stack saved: ${stack.name} (${stack.id})`));
83
+ }
84
+ catch (err) {
85
+ console.error(error(`Failed to save stack: ${err instanceof Error ? err.message : String(err)}`));
86
+ process.exit(1);
87
+ }
88
+ }
89
+ });
90
+ //# sourceMappingURL=analyze.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * stackweld benchmark <stackId> — Show performance profile for a saved stack.
3
+ */
4
+ import { Command } from "commander";
5
+ export declare const benchmarkCommand: Command;
6
+ //# sourceMappingURL=benchmark.d.ts.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * stackweld benchmark <stackId> — Show performance profile for a saved stack.
3
+ */
4
+ import { profilePerformance } from "@stackweld/core";
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+ import { getRulesEngine, getStackEngine } from "../ui/context.js";
8
+ import { box, formatJson, gradientHeader } from "../ui/format.js";
9
+ const RATING_COLORS = {
10
+ blazing: (s) => chalk.green.bold(s),
11
+ fast: (s) => chalk.cyan.bold(s),
12
+ moderate: (s) => chalk.yellow.bold(s),
13
+ heavy: (s) => chalk.red.bold(s),
14
+ };
15
+ const PERF_COLORS = {
16
+ fast: (s) => chalk.green(s),
17
+ moderate: (s) => chalk.yellow(s),
18
+ heavy: (s) => chalk.red(s),
19
+ };
20
+ const RATING_ICONS = {
21
+ blazing: "\u26A1",
22
+ fast: "\u2714",
23
+ moderate: "\u25CF",
24
+ heavy: "\u26A0",
25
+ };
26
+ export const benchmarkCommand = new Command("benchmark")
27
+ .description("Show performance profile for a saved stack")
28
+ .argument("<stack-id>", "Stack ID to profile")
29
+ .option("--json", "Output as JSON")
30
+ .action((stackId, opts) => {
31
+ const engine = getStackEngine();
32
+ const rules = getRulesEngine();
33
+ const stack = engine.get(stackId);
34
+ if (!stack) {
35
+ console.error(chalk.red(`\u2716 Stack "${stackId}" not found.`));
36
+ console.error(chalk.dim(" Run: stackweld list"));
37
+ process.exit(1);
38
+ }
39
+ // Resolve full technology data
40
+ const technologies = stack.technologies
41
+ .map((st) => rules.getTechnology(st.technologyId))
42
+ .filter((t) => t !== undefined);
43
+ if (technologies.length === 0) {
44
+ console.error(chalk.red("\u2716 No technologies could be resolved for this stack."));
45
+ process.exit(1);
46
+ }
47
+ const profile = profilePerformance(technologies);
48
+ if (opts.json) {
49
+ console.log(formatJson(profile));
50
+ return;
51
+ }
52
+ // ── Format output ──
53
+ const ratingColor = RATING_COLORS[profile.rating] || chalk.white;
54
+ const ratingIcon = RATING_ICONS[profile.rating] || "";
55
+ const lines = [];
56
+ lines.push("");
57
+ lines.push(` ${chalk.dim("Rating:")} ${ratingIcon} ${ratingColor(profile.rating.toUpperCase())}`);
58
+ lines.push(` ${chalk.dim("Req/s:")} ${chalk.bold(profile.estimatedReqPerSec)}`);
59
+ lines.push(` ${chalk.dim("Cold start:")} ${profile.estimatedColdStart}`);
60
+ lines.push(` ${chalk.dim("Memory:")} ${profile.estimatedMemory}`);
61
+ lines.push("");
62
+ // Per-technology breakdown
63
+ if (profile.techProfiles.length > 0) {
64
+ lines.push(` ${chalk.bold("Technology Breakdown:")}`);
65
+ lines.push("");
66
+ for (const tp of profile.techProfiles) {
67
+ const perfColor = PERF_COLORS[tp.perf] || chalk.white;
68
+ const perfLabel = perfColor(tp.perf.toUpperCase().padEnd(8));
69
+ lines.push(` ${perfLabel} ${chalk.cyan(tp.name)} ${chalk.dim(`(${tp.category})`)}`);
70
+ lines.push(` ${chalk.dim(tp.note)}`);
71
+ }
72
+ lines.push("");
73
+ }
74
+ // Notes
75
+ if (profile.notes.length > 0) {
76
+ lines.push(` ${chalk.bold("Notes:")}`);
77
+ for (const note of profile.notes) {
78
+ lines.push(` ${chalk.dim("\u2192")} ${chalk.dim(note)}`);
79
+ }
80
+ lines.push("");
81
+ }
82
+ console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ Benchmark")}\n`);
83
+ console.log(box(lines.join("\n"), `Performance: ${stack.name}`));
84
+ console.log("");
85
+ });
86
+ //# sourceMappingURL=benchmark.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * stackweld browse — Browse the technology catalog and templates.
3
+ */
4
+ import { Command } from "commander";
5
+ export declare const browseCommand: Command;
6
+ //# sourceMappingURL=browse.d.ts.map