@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,101 @@
1
+ /**
2
+ * stackweld browse — Browse the technology catalog and templates.
3
+ */
4
+ import { select } from "@inquirer/prompts";
5
+ import { getAllTemplates } from "@stackweld/templates";
6
+ import chalk from "chalk";
7
+ import { Command } from "commander";
8
+ import { getRulesEngine } from "../ui/context.js";
9
+ import { emptyState, formatJson, formatTechTable, formatTemplate, sectionHeader, } from "../ui/format.js";
10
+ export const browseCommand = new Command("browse")
11
+ .description("Browse the technology catalog and templates")
12
+ .option("--category <cat>", "Filter by category")
13
+ .option("--templates", "Show templates only")
14
+ .option("--json", "Output as JSON")
15
+ .action(async (opts) => {
16
+ const rules = getRulesEngine();
17
+ if (opts.templates) {
18
+ const templates = getAllTemplates();
19
+ if (opts.json) {
20
+ console.log(formatJson(templates));
21
+ return;
22
+ }
23
+ if (templates.length === 0) {
24
+ console.log(emptyState("No templates available."));
25
+ return;
26
+ }
27
+ console.log(chalk.bold(`\n ${templates.length} template(s)\n`));
28
+ for (const t of templates) {
29
+ console.log(formatTemplate(t));
30
+ console.log("");
31
+ }
32
+ return;
33
+ }
34
+ const categories = [
35
+ "runtime",
36
+ "frontend",
37
+ "backend",
38
+ "database",
39
+ "orm",
40
+ "auth",
41
+ "styling",
42
+ "service",
43
+ "devops",
44
+ ];
45
+ const category = opts.category ||
46
+ (await select({
47
+ message: "Browse by category:",
48
+ choices: [
49
+ { name: "All technologies", value: "all" },
50
+ ...categories.map((c) => ({ name: c, value: c })),
51
+ { name: "Templates", value: "templates" },
52
+ ],
53
+ }));
54
+ if (category === "templates") {
55
+ const templates = getAllTemplates();
56
+ if (opts.json) {
57
+ console.log(formatJson(templates));
58
+ return;
59
+ }
60
+ if (templates.length === 0) {
61
+ console.log(emptyState("No templates available."));
62
+ return;
63
+ }
64
+ for (const t of templates) {
65
+ console.log(formatTemplate(t));
66
+ console.log("");
67
+ }
68
+ return;
69
+ }
70
+ if (category === "all") {
71
+ // Show all technologies grouped by category
72
+ const allTechs = rules.getAllTechnologies();
73
+ if (opts.json) {
74
+ console.log(formatJson(allTechs));
75
+ return;
76
+ }
77
+ console.log(chalk.bold(`\n ${allTechs.length} technologies in catalog\n`));
78
+ for (const cat of categories) {
79
+ const techs = rules.getByCategory(cat);
80
+ if (techs.length === 0)
81
+ continue;
82
+ console.log(sectionHeader(` ${cat.toUpperCase()} (${techs.length})`));
83
+ console.log(formatTechTable(techs));
84
+ console.log("");
85
+ }
86
+ return;
87
+ }
88
+ const techs = rules.getByCategory(category);
89
+ if (opts.json) {
90
+ console.log(formatJson(techs));
91
+ return;
92
+ }
93
+ if (techs.length === 0) {
94
+ console.log(emptyState("No technologies in this category."));
95
+ return;
96
+ }
97
+ console.log(chalk.bold(`\n ${techs.length} ${category} technology(ies)\n`));
98
+ console.log(formatTechTable(techs));
99
+ console.log("");
100
+ });
101
+ //# sourceMappingURL=browse.js.map
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare const cloneCommand: Command;
3
+ //# sourceMappingURL=clone.d.ts.map
@@ -0,0 +1,37 @@
1
+ import { input } from "@inquirer/prompts";
2
+ import chalk from "chalk";
3
+ import { Command } from "commander";
4
+ import { getStackEngine } from "../ui/context.js";
5
+ import { formatStackRow } from "../ui/format.js";
6
+ export const cloneCommand = new Command("clone")
7
+ .description("Duplicate a saved stack")
8
+ .argument("<stack-id>", "Stack ID to clone")
9
+ .option("--name <name>", "Name for the cloned stack")
10
+ .action(async (stackId, opts) => {
11
+ const engine = getStackEngine();
12
+ const source = engine.get(stackId);
13
+ if (!source) {
14
+ console.error(chalk.red(`Stack "${stackId}" not found.`));
15
+ process.exit(1);
16
+ }
17
+ const name = opts.name ||
18
+ (await input({
19
+ message: "Name for the cloned stack:",
20
+ default: `${source.name} (copy)`,
21
+ }));
22
+ const { stack, validation } = engine.create({
23
+ name,
24
+ description: source.description,
25
+ profile: source.profile,
26
+ technologies: source.technologies,
27
+ tags: [...source.tags, "cloned"],
28
+ });
29
+ if (validation.valid) {
30
+ console.log(chalk.green(`✓ Cloned "${source.name}" as "${name}"`));
31
+ console.log(formatStackRow(stack));
32
+ }
33
+ else {
34
+ console.error(chalk.red("Clone failed validation."));
35
+ }
36
+ });
37
+ //# sourceMappingURL=clone.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * stackweld compare <stackA> <stackB> — Compare two saved stacks side by side.
3
+ */
4
+ import { Command } from "commander";
5
+ export declare const compareCommand: Command;
6
+ //# sourceMappingURL=compare.d.ts.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * stackweld compare <stackA> <stackB> — Compare two saved stacks side by side.
3
+ */
4
+ import { diffStacks } from "@stackweld/core";
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+ import { getStackEngine } from "../ui/context.js";
8
+ import { box, formatJson, gradientHeader } from "../ui/format.js";
9
+ export const compareCommand = new Command("compare")
10
+ .description("Compare two saved stacks")
11
+ .argument("<stackA>", "First stack ID")
12
+ .argument("<stackB>", "Second stack ID")
13
+ .option("--json", "Output as JSON")
14
+ .action((stackAId, stackBId, opts) => {
15
+ const engine = getStackEngine();
16
+ const stackA = engine.get(stackAId);
17
+ if (!stackA) {
18
+ console.error(chalk.red(`\u2716 Stack "${stackAId}" not found.`));
19
+ console.error(chalk.dim(" Run `stackweld list` to see available stacks."));
20
+ process.exit(1);
21
+ }
22
+ const stackB = engine.get(stackBId);
23
+ if (!stackB) {
24
+ console.error(chalk.red(`\u2716 Stack "${stackBId}" not found.`));
25
+ console.error(chalk.dim(" Run `stackweld list` to see available stacks."));
26
+ process.exit(1);
27
+ }
28
+ const diff = diffStacks(stackA, stackB);
29
+ if (opts.json) {
30
+ console.log(formatJson({
31
+ stackA: { id: stackA.id, name: stackA.name },
32
+ stackB: { id: stackB.id, name: stackB.name },
33
+ ...diff,
34
+ }));
35
+ return;
36
+ }
37
+ const lines = [];
38
+ lines.push("");
39
+ // Added
40
+ if (diff.added.length > 0) {
41
+ lines.push(chalk.green.bold(` + Added in B (${diff.added.length}):`));
42
+ for (const item of diff.added) {
43
+ const ver = item.version ? ` ${item.version}` : "";
44
+ lines.push(chalk.green(` + ${item.name}${ver}`));
45
+ }
46
+ lines.push("");
47
+ }
48
+ // Removed
49
+ if (diff.removed.length > 0) {
50
+ lines.push(chalk.red.bold(` - Removed from A (${diff.removed.length}):`));
51
+ for (const item of diff.removed) {
52
+ const ver = item.version ? ` ${item.version}` : "";
53
+ lines.push(chalk.red(` - ${item.name}${ver}`));
54
+ }
55
+ lines.push("");
56
+ }
57
+ // Changed
58
+ if (diff.changed.length > 0) {
59
+ // Group changes by technology
60
+ const byTech = new Map();
61
+ for (const change of diff.changed) {
62
+ const existing = byTech.get(change.technologyId) || [];
63
+ existing.push(change);
64
+ byTech.set(change.technologyId, existing);
65
+ }
66
+ lines.push(chalk.yellow.bold(` ~ Changed (${byTech.size}):`));
67
+ for (const [, changes] of byTech) {
68
+ for (const change of changes) {
69
+ lines.push(chalk.yellow(` ~ ${change.name}: ${change.from} \u2192 ${change.to}`) +
70
+ chalk.dim(` (${change.field})`));
71
+ }
72
+ }
73
+ lines.push("");
74
+ }
75
+ // Unchanged
76
+ if (diff.unchanged.length > 0) {
77
+ lines.push(chalk.dim(` = Unchanged (${diff.unchanged.length}):`));
78
+ lines.push(chalk.dim(` ${diff.unchanged.join(", ")}`));
79
+ lines.push("");
80
+ }
81
+ // No differences
82
+ if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) {
83
+ lines.push(chalk.green(" Stacks are identical."));
84
+ lines.push("");
85
+ }
86
+ lines.push(` ${chalk.dim("Summary:")} ${diff.summary}`);
87
+ lines.push("");
88
+ const title = `A: ${stackA.name} vs B: ${stackB.name}`;
89
+ console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ Stack Comparison")}\n`);
90
+ console.log(box(lines.join("\n"), title));
91
+ console.log("");
92
+ });
93
+ //# sourceMappingURL=compare.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * stackweld completion — Generate shell completions.
3
+ */
4
+ import { Command } from "commander";
5
+ export declare const completionCommand: Command;
6
+ //# sourceMappingURL=completion.d.ts.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * stackweld completion — Generate shell completions.
3
+ */
4
+ import { Command } from "commander";
5
+ const BASH_COMPLETION = `
6
+ # stackweld bash completion
7
+ _stackweld_completions() {
8
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
9
+ local commands="init create list info save export import browse doctor delete version up down status logs scaffold completion"
10
+
11
+ if [ "\${COMP_CWORD}" -eq 1 ]; then
12
+ COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
13
+ fi
14
+ }
15
+ complete -F _stackweld_completions stackweld
16
+ `.trim();
17
+ const ZSH_COMPLETION = `
18
+ #compdef stackweld
19
+
20
+ _stackweld() {
21
+ local -a commands
22
+ commands=(
23
+ 'init:Create a new stack interactively'
24
+ 'create:Scaffold a project from a stack or template'
25
+ 'list:List all saved stacks'
26
+ 'info:Show details about a stack or technology'
27
+ 'save:Save a version snapshot of a stack'
28
+ 'export:Export a stack definition to YAML or JSON'
29
+ 'import:Import a stack definition from a file'
30
+ 'browse:Browse the technology catalog and templates'
31
+ 'doctor:Check system requirements and environment'
32
+ 'delete:Delete a saved stack'
33
+ 'version:Manage stack versions'
34
+ 'up:Start Docker services'
35
+ 'down:Stop Docker services'
36
+ 'status:Show status of running services'
37
+ 'logs:Show logs from Docker services'
38
+ 'scaffold:Generate project files from a stack'
39
+ 'completion:Generate shell completions'
40
+ )
41
+
42
+ _describe 'command' commands
43
+ }
44
+
45
+ _stackweld
46
+ `.trim();
47
+ const FISH_COMPLETION = `
48
+ # stackweld fish completions
49
+ complete -c stackweld -n "__fish_use_subcommand" -a init -d "Create a new stack interactively"
50
+ complete -c stackweld -n "__fish_use_subcommand" -a create -d "Scaffold a project"
51
+ complete -c stackweld -n "__fish_use_subcommand" -a list -d "List all saved stacks"
52
+ complete -c stackweld -n "__fish_use_subcommand" -a info -d "Show details about a stack or technology"
53
+ complete -c stackweld -n "__fish_use_subcommand" -a save -d "Save a version snapshot"
54
+ complete -c stackweld -n "__fish_use_subcommand" -a export -d "Export a stack definition"
55
+ complete -c stackweld -n "__fish_use_subcommand" -a import -d "Import a stack definition"
56
+ complete -c stackweld -n "__fish_use_subcommand" -a browse -d "Browse the technology catalog"
57
+ complete -c stackweld -n "__fish_use_subcommand" -a doctor -d "Check system requirements"
58
+ complete -c stackweld -n "__fish_use_subcommand" -a delete -d "Delete a saved stack"
59
+ complete -c stackweld -n "__fish_use_subcommand" -a version -d "Manage stack versions"
60
+ complete -c stackweld -n "__fish_use_subcommand" -a up -d "Start Docker services"
61
+ complete -c stackweld -n "__fish_use_subcommand" -a down -d "Stop Docker services"
62
+ complete -c stackweld -n "__fish_use_subcommand" -a status -d "Show status of running services"
63
+ complete -c stackweld -n "__fish_use_subcommand" -a logs -d "Show logs from Docker services"
64
+ complete -c stackweld -n "__fish_use_subcommand" -a scaffold -d "Generate project files"
65
+ complete -c stackweld -n "__fish_use_subcommand" -a completion -d "Generate shell completions"
66
+ `.trim();
67
+ export const completionCommand = new Command("completion")
68
+ .description("Generate shell completion scripts")
69
+ .argument("<shell>", "Shell type: bash, zsh, or fish")
70
+ .action((shell) => {
71
+ switch (shell) {
72
+ case "bash":
73
+ console.log(BASH_COMPLETION);
74
+ break;
75
+ case "zsh":
76
+ console.log(ZSH_COMPLETION);
77
+ break;
78
+ case "fish":
79
+ console.log(FISH_COMPLETION);
80
+ break;
81
+ default:
82
+ console.error(`Unknown shell: ${shell}. Use bash, zsh, or fish.`);
83
+ process.exit(1);
84
+ }
85
+ });
86
+ //# sourceMappingURL=completion.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * stackweld config — Manage user preferences.
3
+ */
4
+ import { Command } from "commander";
5
+ export declare const configCommand: Command;
6
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * stackweld config — Manage user preferences.
3
+ */
4
+ import { getDefaultPreferences, getPreferenceKeys, getPreferences, resetPreferences, setPreference, } from "@stackweld/core";
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+ import { formatJson } from "../ui/format.js";
8
+ export const configCommand = new Command("config")
9
+ .description("Manage user preferences")
10
+ .addCommand(new Command("list")
11
+ .description("Show all preferences")
12
+ .option("--json", "Output as JSON")
13
+ .action((opts) => {
14
+ const prefs = getPreferences();
15
+ const defaults = getDefaultPreferences();
16
+ if (opts.json) {
17
+ console.log(formatJson(prefs));
18
+ return;
19
+ }
20
+ console.log(chalk.bold("Preferences:\n"));
21
+ for (const key of getPreferenceKeys()) {
22
+ const value = prefs[key];
23
+ const isDefault = value === defaults[key];
24
+ console.log(` ${chalk.cyan(key)}: ${value} ${isDefault ? chalk.dim("(default)") : ""}`);
25
+ }
26
+ }))
27
+ .addCommand(new Command("set")
28
+ .description("Set a preference")
29
+ .argument("<key>", "Preference key")
30
+ .argument("<value>", "Preference value")
31
+ .action((key, value) => {
32
+ const validKeys = getPreferenceKeys();
33
+ if (!validKeys.includes(key)) {
34
+ console.error(chalk.red(`Invalid key "${key}". Valid keys: ${validKeys.join(", ")}`));
35
+ process.exit(1);
36
+ }
37
+ setPreference(key, value);
38
+ console.log(chalk.green(`✓ Set ${key} = ${value}`));
39
+ }))
40
+ .addCommand(new Command("get")
41
+ .description("Get a preference value")
42
+ .argument("<key>", "Preference key")
43
+ .action((key) => {
44
+ const prefs = getPreferences();
45
+ const validKeys = getPreferenceKeys();
46
+ if (!validKeys.includes(key)) {
47
+ console.error(chalk.red(`Invalid key "${key}".`));
48
+ process.exit(1);
49
+ }
50
+ console.log(prefs[key]);
51
+ }))
52
+ .addCommand(new Command("reset").description("Reset all preferences to defaults").action(() => {
53
+ resetPreferences();
54
+ console.log(chalk.green("✓ Preferences reset to defaults."));
55
+ }));
56
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * stackweld cost <stackId> — Estimate monthly hosting costs for a saved stack.
3
+ */
4
+ import { Command } from "commander";
5
+ export declare const costCommand: Command;
6
+ //# sourceMappingURL=cost.d.ts.map
@@ -0,0 +1,101 @@
1
+ /**
2
+ * stackweld cost <stackId> — Estimate monthly hosting costs for a saved stack.
3
+ */
4
+ import { estimateCost } 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, table } from "../ui/format.js";
9
+ const TIER_COLORS = {
10
+ free: (s) => chalk.green.bold(s),
11
+ budget: (s) => chalk.cyan.bold(s),
12
+ standard: (s) => chalk.yellow.bold(s),
13
+ premium: (s) => chalk.magenta.bold(s),
14
+ };
15
+ const TIER_ICONS = {
16
+ free: "\u2728",
17
+ budget: "\uD83D\uDCB0",
18
+ standard: "\uD83D\uDCB3",
19
+ premium: "\uD83D\uDC8E",
20
+ };
21
+ export const costCommand = new Command("cost")
22
+ .description("Estimate monthly hosting costs for a saved stack")
23
+ .argument("<stack-id>", "Stack ID to estimate costs for")
24
+ .option("--json", "Output as JSON")
25
+ .action((stackId, opts) => {
26
+ const engine = getStackEngine();
27
+ const rules = getRulesEngine();
28
+ const stack = engine.get(stackId);
29
+ if (!stack) {
30
+ console.error(chalk.red(`\u2716 Stack "${stackId}" not found.`));
31
+ console.error(chalk.dim(" Run: stackweld list"));
32
+ process.exit(1);
33
+ }
34
+ // Resolve full technology data
35
+ const technologies = stack.technologies
36
+ .map((st) => rules.getTechnology(st.technologyId))
37
+ .filter((t) => t !== undefined);
38
+ if (technologies.length === 0) {
39
+ console.error(chalk.red("\u2716 No technologies could be resolved for this stack."));
40
+ process.exit(1);
41
+ }
42
+ const estimate = estimateCost(technologies);
43
+ if (opts.json) {
44
+ console.log(formatJson(estimate));
45
+ return;
46
+ }
47
+ // ── Format output ──
48
+ const tierColor = TIER_COLORS[estimate.tier] || chalk.white;
49
+ const tierIcon = TIER_ICONS[estimate.tier] || "";
50
+ const lines = [];
51
+ lines.push("");
52
+ // Cost range
53
+ const { min, max, currency } = estimate.monthly;
54
+ const rangeStr = min === max ? `$${min}/mo` : `$${min} - $${max}/mo`;
55
+ lines.push(` ${chalk.dim("Estimated:")} ${chalk.bold.green(rangeStr)} ${chalk.dim(`(${currency})`)}`);
56
+ lines.push(` ${chalk.dim("Tier:")} ${tierIcon} ${tierColor(estimate.tier.toUpperCase())}`);
57
+ lines.push("");
58
+ // Breakdown table
59
+ // Deduplicate by service+provider for display
60
+ const seen = new Set();
61
+ const uniqueItems = estimate.breakdown.filter((item) => {
62
+ const key = `${item.service}|${item.provider}`;
63
+ if (seen.has(key))
64
+ return false;
65
+ seen.add(key);
66
+ return true;
67
+ });
68
+ const tableData = uniqueItems.map((item) => ({
69
+ service: item.service,
70
+ provider: item.provider,
71
+ cost: item.monthlyCost,
72
+ notes: item.notes.length > 35 ? `${item.notes.slice(0, 32)}...` : item.notes,
73
+ }));
74
+ if (tableData.length > 0) {
75
+ lines.push(` ${chalk.bold("Cost Breakdown:")}`);
76
+ lines.push("");
77
+ const tbl = table(tableData, [
78
+ { header: "Service", key: "service", color: (v) => chalk.cyan(v) },
79
+ { header: "Provider", key: "provider" },
80
+ { header: "Cost", key: "cost", color: (v) => chalk.green(v) },
81
+ { header: "Notes", key: "notes", color: (v) => chalk.dim(v) },
82
+ ]);
83
+ // Indent table lines
84
+ for (const line of tbl.split("\n")) {
85
+ lines.push(` ${line}`);
86
+ }
87
+ lines.push("");
88
+ }
89
+ // Notes
90
+ if (estimate.notes.length > 0) {
91
+ lines.push(` ${chalk.bold("Notes:")}`);
92
+ for (const note of estimate.notes) {
93
+ lines.push(` ${chalk.dim("\u2192")} ${chalk.dim(note)}`);
94
+ }
95
+ lines.push("");
96
+ }
97
+ console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ Cost Estimator")}\n`);
98
+ console.log(box(lines.join("\n"), `Cost Estimate: ${stack.name}`));
99
+ console.log("");
100
+ });
101
+ //# sourceMappingURL=cost.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * stackweld create <stack-id> --path <dir> — Scaffold a stack to disk.
3
+ * Uses official CLI tools when available and fills gaps.
4
+ */
5
+ import { Command } from "commander";
6
+ export declare const createCommand: Command;
7
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1,111 @@
1
+ /**
2
+ * stackweld create <stack-id> --path <dir> — Scaffold a stack to disk.
3
+ * Uses official CLI tools when available and fills gaps.
4
+ */
5
+ import { execSync } from "node:child_process";
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { getAllTemplates, getTemplate } from "@stackweld/templates";
9
+ import chalk from "chalk";
10
+ import { Command } from "commander";
11
+ import ora from "ora";
12
+ import { getStackEngine } from "../ui/context.js";
13
+ export const createCommand = new Command("create")
14
+ .description("Scaffold a project from a stack or template")
15
+ .argument("<id>", "Stack ID or template ID")
16
+ .option("-p, --path <dir>", "Target directory", ".")
17
+ .option("--dry-run", "Show what would be executed without doing it")
18
+ .option("--json", "Output result as JSON")
19
+ .action(async (id, opts) => {
20
+ const engine = getStackEngine();
21
+ // Check if it's a template
22
+ const template = getTemplate(id);
23
+ const stack = engine.get(id);
24
+ if (!template && !stack) {
25
+ console.error(chalk.red(`"${id}" is not a valid stack ID or template ID.`));
26
+ console.log(chalk.dim("Available templates: " +
27
+ getAllTemplates()
28
+ .map((t) => t.id)
29
+ .join(", ")));
30
+ process.exit(1);
31
+ }
32
+ if (template) {
33
+ console.log(chalk.cyan(`Using template: ${template.name}`));
34
+ const projectName = path.basename(opts.path === "." ? template.variables.projectName || "my-project" : opts.path);
35
+ const targetDir = path.resolve(opts.path === "." ? projectName : opts.path);
36
+ // Execute scaffold steps
37
+ for (const step of template.scaffoldSteps) {
38
+ const command = step.command.replace(/\{\{projectName\}\}/g, projectName);
39
+ const cwd = step.workingDir
40
+ ? path.resolve(step.workingDir.replace(/\{\{projectName\}\}/g, projectName))
41
+ : process.cwd();
42
+ if (opts.dryRun) {
43
+ console.log(chalk.dim(`[dry-run] ${step.name}: ${command}`));
44
+ continue;
45
+ }
46
+ const spinner = ora(step.name).start();
47
+ try {
48
+ execSync(command, { cwd, stdio: "pipe" });
49
+ spinner.succeed(step.name);
50
+ }
51
+ catch (err) {
52
+ spinner.fail(step.name);
53
+ console.error(chalk.red(` Command failed: ${command}`));
54
+ if (err instanceof Error) {
55
+ console.error(chalk.dim(err.message.slice(0, 200)));
56
+ }
57
+ }
58
+ }
59
+ // Apply overrides
60
+ for (const override of template.overrides) {
61
+ const filePath = path.join(targetDir, override.path.replace(/\{\{projectName\}\}/g, projectName));
62
+ if (opts.dryRun) {
63
+ console.log(chalk.dim(`[dry-run] Write: ${filePath}`));
64
+ continue;
65
+ }
66
+ const dir = path.dirname(filePath);
67
+ if (!fs.existsSync(dir))
68
+ fs.mkdirSync(dir, { recursive: true });
69
+ const content = override.content.replace(/\{\{projectName\}\}/g, projectName);
70
+ fs.writeFileSync(filePath, content, "utf-8");
71
+ console.log(chalk.green(` ✓ ${override.path}`));
72
+ }
73
+ // Execute hooks
74
+ for (const hook of template.hooks) {
75
+ const command = hook.command.replace(/\{\{projectName\}\}/g, projectName);
76
+ if (opts.dryRun) {
77
+ console.log(chalk.dim(`[dry-run] Hook (${hook.timing}): ${command}`));
78
+ continue;
79
+ }
80
+ if (hook.requiresConfirmation) {
81
+ console.log(chalk.yellow(`Hook requires confirmation: ${hook.description}`));
82
+ console.log(chalk.dim(` Command: ${command}`));
83
+ // In a real implementation, we'd prompt for confirmation here
84
+ continue;
85
+ }
86
+ const spinner = ora(`${hook.timing}: ${hook.name}`).start();
87
+ try {
88
+ execSync(command, { stdio: "pipe" });
89
+ spinner.succeed(`${hook.timing}: ${hook.name}`);
90
+ }
91
+ catch {
92
+ spinner.fail(`${hook.timing}: ${hook.name}`);
93
+ }
94
+ }
95
+ if (!opts.dryRun) {
96
+ console.log("");
97
+ console.log(chalk.green(`✓ Project "${projectName}" created successfully!`));
98
+ console.log(chalk.dim(` cd ${projectName}`));
99
+ }
100
+ }
101
+ else if (stack) {
102
+ if (opts.json) {
103
+ console.log(JSON.stringify(stack, null, 2));
104
+ }
105
+ else {
106
+ console.log(chalk.cyan(`Stack: ${stack.name}`));
107
+ console.log(chalk.dim("Scaffold orchestration for saved stacks is coming in the next release."));
108
+ }
109
+ }
110
+ });
111
+ //# sourceMappingURL=create.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * stackweld delete <id> — Delete a saved stack.
3
+ */
4
+ import { Command } from "commander";
5
+ export declare const deleteCommand: Command;
6
+ //# sourceMappingURL=delete.d.ts.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * stackweld delete <id> — Delete a saved stack.
3
+ */
4
+ import { confirm } from "@inquirer/prompts";
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+ import { getStackEngine } from "../ui/context.js";
8
+ export const deleteCommand = new Command("delete")
9
+ .aliases(["rm"])
10
+ .description("Delete a saved stack")
11
+ .argument("<id>", "Stack ID")
12
+ .option("-f, --force", "Skip confirmation")
13
+ .action(async (id, opts) => {
14
+ const engine = getStackEngine();
15
+ const stack = engine.get(id);
16
+ if (!stack) {
17
+ console.error(chalk.red(`Stack "${id}" not found.`));
18
+ process.exit(1);
19
+ }
20
+ if (!opts.force) {
21
+ const yes = await confirm({
22
+ message: `Delete stack "${stack.name}"? This cannot be undone.`,
23
+ default: false,
24
+ });
25
+ if (!yes) {
26
+ console.log(chalk.dim("Cancelled."));
27
+ return;
28
+ }
29
+ }
30
+ engine.delete(id);
31
+ console.log(chalk.green(`✓ Stack "${stack.name}" deleted.`));
32
+ });
33
+ //# sourceMappingURL=delete.js.map