@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.
- package/dist/__tests__/commands.test.d.ts +2 -0
- package/dist/__tests__/commands.test.js +275 -0
- package/dist/commands/ai.d.ts +8 -0
- package/dist/commands/ai.js +167 -0
- package/dist/commands/analyze.d.ts +6 -0
- package/dist/commands/analyze.js +90 -0
- package/dist/commands/benchmark.d.ts +6 -0
- package/dist/commands/benchmark.js +86 -0
- package/dist/commands/browse.d.ts +6 -0
- package/dist/commands/browse.js +101 -0
- package/dist/commands/clone.d.ts +3 -0
- package/dist/commands/clone.js +37 -0
- package/dist/commands/compare.d.ts +6 -0
- package/dist/commands/compare.js +93 -0
- package/dist/commands/completion.d.ts +6 -0
- package/dist/commands/completion.js +86 -0
- package/dist/commands/config.d.ts +6 -0
- package/dist/commands/config.js +56 -0
- package/dist/commands/cost.d.ts +6 -0
- package/dist/commands/cost.js +101 -0
- package/dist/commands/create.d.ts +7 -0
- package/dist/commands/create.js +111 -0
- package/dist/commands/delete.d.ts +6 -0
- package/dist/commands/delete.js +33 -0
- package/dist/commands/deploy.d.ts +6 -0
- package/dist/commands/deploy.js +90 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.js +144 -0
- package/dist/commands/down.d.ts +6 -0
- package/dist/commands/down.js +37 -0
- package/dist/commands/env.d.ts +6 -0
- package/dist/commands/env.js +129 -0
- package/dist/commands/export-stack.d.ts +6 -0
- package/dist/commands/export-stack.js +51 -0
- package/dist/commands/generate.d.ts +9 -0
- package/dist/commands/generate.js +542 -0
- package/dist/commands/health.d.ts +6 -0
- package/dist/commands/health.js +68 -0
- package/dist/commands/import-stack.d.ts +6 -0
- package/dist/commands/import-stack.js +68 -0
- package/dist/commands/info.d.ts +6 -0
- package/dist/commands/info.js +56 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +186 -0
- package/dist/commands/learn.d.ts +6 -0
- package/dist/commands/learn.js +91 -0
- package/dist/commands/lint.d.ts +6 -0
- package/dist/commands/lint.js +193 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.js +27 -0
- package/dist/commands/logs.d.ts +6 -0
- package/dist/commands/logs.js +37 -0
- package/dist/commands/migrate.d.ts +6 -0
- package/dist/commands/migrate.js +57 -0
- package/dist/commands/plugin.d.ts +8 -0
- package/dist/commands/plugin.js +131 -0
- package/dist/commands/preview.d.ts +7 -0
- package/dist/commands/preview.js +100 -0
- package/dist/commands/save.d.ts +6 -0
- package/dist/commands/save.js +32 -0
- package/dist/commands/scaffold.d.ts +7 -0
- package/dist/commands/scaffold.js +100 -0
- package/dist/commands/score.d.ts +9 -0
- package/dist/commands/score.js +111 -0
- package/dist/commands/share.d.ts +10 -0
- package/dist/commands/share.js +93 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +39 -0
- package/dist/commands/template.d.ts +3 -0
- package/dist/commands/template.js +162 -0
- package/dist/commands/up.d.ts +6 -0
- package/dist/commands/up.js +54 -0
- package/dist/commands/version-cmd.d.ts +6 -0
- package/dist/commands/version-cmd.js +100 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +160 -0
- package/dist/ui/context.d.ts +10 -0
- package/dist/ui/context.js +90 -0
- package/dist/ui/format.d.ts +59 -0
- package/dist/ui/format.js +295 -0
- 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,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,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,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,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,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,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,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
|