@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,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld logs [service] — Show logs from Docker services.
|
|
3
|
+
*/
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { getRuntimeManager } from "../ui/context.js";
|
|
8
|
+
import { error } from "../ui/format.js";
|
|
9
|
+
export const logsCommand = new Command("logs")
|
|
10
|
+
.description("Show logs from Docker services")
|
|
11
|
+
.argument("[service]", "Service name (optional, shows all if omitted)")
|
|
12
|
+
.option("-p, --path <dir>", "Project directory", ".")
|
|
13
|
+
.option("-n, --tail <lines>", "Number of lines to show", "50")
|
|
14
|
+
.option("-f, --follow", "Follow log output")
|
|
15
|
+
.action((service, opts) => {
|
|
16
|
+
const runtime = getRuntimeManager();
|
|
17
|
+
const projectDir = path.resolve(opts.path);
|
|
18
|
+
if (!runtime.isDockerAvailable()) {
|
|
19
|
+
console.error(error("Docker is not available."));
|
|
20
|
+
console.error(chalk.dim(" Install Docker: https://docs.docker.com/get-docker/"));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const composePath = runtime.composeExists(projectDir);
|
|
24
|
+
if (!composePath) {
|
|
25
|
+
console.error(error(`No docker-compose.yml found in ${projectDir}`));
|
|
26
|
+
console.error(chalk.dim(" Run `stackweld scaffold` to generate one from a stack."));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const tail = Number.parseInt(opts.tail, 10) || 50;
|
|
30
|
+
const output = runtime.logs({ composePath, projectDir }, service, tail, opts.follow);
|
|
31
|
+
if (!output || output.trim().length === 0) {
|
|
32
|
+
console.log(chalk.dim(service ? `No logs for service "${service}".` : "No logs available."));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(output);
|
|
36
|
+
});
|
|
37
|
+
//# sourceMappingURL=logs.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld migrate --from <techId> --to <techId> — Generate a migration plan between technologies.
|
|
3
|
+
*/
|
|
4
|
+
import { planMigration } from "@stackweld/core";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { getRulesEngine } from "../ui/context.js";
|
|
8
|
+
import { box } from "../ui/format.js";
|
|
9
|
+
const DIFFICULTY_COLORS = {
|
|
10
|
+
easy: chalk.green,
|
|
11
|
+
moderate: chalk.yellow,
|
|
12
|
+
hard: chalk.red,
|
|
13
|
+
expert: chalk.magenta,
|
|
14
|
+
};
|
|
15
|
+
export const migrateCommand = new Command("migrate")
|
|
16
|
+
.description("Generate a migration plan between two technologies")
|
|
17
|
+
.requiredOption("--from <techId>", "Source technology ID")
|
|
18
|
+
.requiredOption("--to <techId>", "Target technology ID")
|
|
19
|
+
.option("--json", "Output as JSON")
|
|
20
|
+
.action((opts) => {
|
|
21
|
+
const rules = getRulesEngine();
|
|
22
|
+
const techs = rules.getAllTechnologies();
|
|
23
|
+
const fromId = opts.from;
|
|
24
|
+
const toId = opts.to;
|
|
25
|
+
if (fromId === toId) {
|
|
26
|
+
console.error(chalk.red("\u2716 Source and target technologies must be different."));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const plan = planMigration(fromId, toId, techs);
|
|
30
|
+
if (opts.json) {
|
|
31
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const diffColor = DIFFICULTY_COLORS[plan.difficulty] ?? chalk.white;
|
|
35
|
+
const lines = [];
|
|
36
|
+
lines.push("");
|
|
37
|
+
for (const step of plan.steps) {
|
|
38
|
+
lines.push(chalk.bold(` Step ${step.order}: ${step.title}`));
|
|
39
|
+
lines.push(` ${chalk.dim(step.description)}`);
|
|
40
|
+
if (step.commands && step.commands.length > 0) {
|
|
41
|
+
for (const cmd of step.commands) {
|
|
42
|
+
lines.push(` ${chalk.cyan(cmd)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (step.files && step.files.length > 0) {
|
|
46
|
+
lines.push(` ${chalk.dim("Files:")} ${step.files.join(", ")}`);
|
|
47
|
+
}
|
|
48
|
+
lines.push("");
|
|
49
|
+
}
|
|
50
|
+
const title = `Migration Plan: ${plan.from.name} \u2192 ${plan.to.name}`;
|
|
51
|
+
const subtitle = `Difficulty: ${diffColor(plan.difficulty.charAt(0).toUpperCase() + plan.difficulty.slice(1))} ${chalk.dim("\u00B7")} Est: ${plan.estimatedTime}`;
|
|
52
|
+
// Build a custom header with title + subtitle
|
|
53
|
+
const header = `${title}\n ${subtitle}`;
|
|
54
|
+
console.log(box(lines.join("\n"), header));
|
|
55
|
+
console.log("");
|
|
56
|
+
});
|
|
57
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld plugin — Manage Stackweld plugins.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands: list, install, remove, info.
|
|
5
|
+
*/
|
|
6
|
+
import { getPluginDir, getPluginInfo, installPlugin, listPlugins, removePlugin, } from "@stackweld/core";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { box, emptyState, formatJson, gradientHeader, table } from "../ui/format.js";
|
|
10
|
+
// ─── plugin list ──────────────────────────────────────
|
|
11
|
+
const pluginListCommand = new Command("list")
|
|
12
|
+
.description("List installed plugins")
|
|
13
|
+
.option("--json", "Output as JSON")
|
|
14
|
+
.action((opts) => {
|
|
15
|
+
const plugins = listPlugins();
|
|
16
|
+
if (opts.json) {
|
|
17
|
+
console.log(formatJson(plugins));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ Plugins")}\n`);
|
|
21
|
+
if (plugins.length === 0) {
|
|
22
|
+
console.log(emptyState("No plugins installed.", "Install one with: stackweld plugin install <path>"));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const data = plugins.map((p) => ({
|
|
26
|
+
name: p.name,
|
|
27
|
+
version: p.version,
|
|
28
|
+
type: p.type,
|
|
29
|
+
description: p.description.length > 40 ? `${p.description.slice(0, 37)}...` : p.description,
|
|
30
|
+
}));
|
|
31
|
+
const tbl = table(data, [
|
|
32
|
+
{ header: "Name", key: "name", color: (v) => chalk.cyan(v) },
|
|
33
|
+
{ header: "Version", key: "version" },
|
|
34
|
+
{ header: "Type", key: "type", color: (v) => chalk.yellow(v) },
|
|
35
|
+
{ header: "Description", key: "description", color: (v) => chalk.dim(v) },
|
|
36
|
+
]);
|
|
37
|
+
console.log(` ${chalk.dim("Plugin directory:")} ${getPluginDir()}\n`);
|
|
38
|
+
console.log(tbl);
|
|
39
|
+
console.log(`\n ${chalk.dim(`${plugins.length} plugin(s) installed.`)}\n`);
|
|
40
|
+
});
|
|
41
|
+
// ─── plugin install ───────────────────────────────────
|
|
42
|
+
const pluginInstallCommand = new Command("install")
|
|
43
|
+
.description("Install a plugin from a local directory")
|
|
44
|
+
.argument("<path>", "Path to plugin directory")
|
|
45
|
+
.option("--json", "Output as JSON")
|
|
46
|
+
.action((pluginPath, opts) => {
|
|
47
|
+
try {
|
|
48
|
+
const manifest = installPlugin(pluginPath);
|
|
49
|
+
if (opts.json) {
|
|
50
|
+
console.log(formatJson({ installed: true, plugin: manifest }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ Plugin Install")}\n`);
|
|
54
|
+
console.log(` ${chalk.green("\u2714")} Plugin ${chalk.cyan.bold(manifest.name)} v${manifest.version} installed successfully.`);
|
|
55
|
+
console.log(` ${chalk.dim("Type:")} ${manifest.type}`);
|
|
56
|
+
console.log(` ${chalk.dim("Description:")} ${manifest.description}`);
|
|
57
|
+
console.log(` ${chalk.dim("Location:")} ${getPluginDir()}/${manifest.name}`);
|
|
58
|
+
console.log("");
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (opts.json) {
|
|
62
|
+
console.log(formatJson({ installed: false, error: err instanceof Error ? err.message : String(err) }));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
console.error(chalk.red(`\u2716 ${err instanceof Error ? err.message : String(err)}`));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// ─── plugin remove ────────────────────────────────────
|
|
70
|
+
const pluginRemoveCommand = new Command("remove")
|
|
71
|
+
.description("Remove an installed plugin")
|
|
72
|
+
.argument("<name>", "Plugin name")
|
|
73
|
+
.option("--json", "Output as JSON")
|
|
74
|
+
.action((name, opts) => {
|
|
75
|
+
try {
|
|
76
|
+
removePlugin(name);
|
|
77
|
+
if (opts.json) {
|
|
78
|
+
console.log(formatJson({ removed: true, name }));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(`\n ${chalk.green("\u2714")} Plugin ${chalk.cyan.bold(name)} removed.\n`);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (opts.json) {
|
|
85
|
+
console.log(formatJson({ removed: false, error: err instanceof Error ? err.message : String(err) }));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
console.error(chalk.red(`\u2716 ${err instanceof Error ? err.message : String(err)}`));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// ─── plugin info ──────────────────────────────────────
|
|
93
|
+
const pluginInfoCommand = new Command("info")
|
|
94
|
+
.description("Show details about an installed plugin")
|
|
95
|
+
.argument("<name>", "Plugin name")
|
|
96
|
+
.option("--json", "Output as JSON")
|
|
97
|
+
.action((name, opts) => {
|
|
98
|
+
const manifest = getPluginInfo(name);
|
|
99
|
+
if (!manifest) {
|
|
100
|
+
if (opts.json) {
|
|
101
|
+
console.log(formatJson({ found: false, name }));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
console.error(chalk.red(`\u2716 Plugin "${name}" not found.`));
|
|
105
|
+
console.error(chalk.dim(" Run: stackweld plugin list"));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
if (opts.json) {
|
|
109
|
+
console.log(formatJson(manifest));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const lines = [
|
|
113
|
+
`${chalk.dim("Name:")} ${chalk.cyan.bold(manifest.name)}`,
|
|
114
|
+
`${chalk.dim("Version:")} ${manifest.version}`,
|
|
115
|
+
`${chalk.dim("Type:")} ${chalk.yellow(manifest.type)}`,
|
|
116
|
+
`${chalk.dim("Description:")} ${manifest.description}`,
|
|
117
|
+
`${chalk.dim("Entry:")} ${manifest.main}`,
|
|
118
|
+
`${chalk.dim("Location:")} ${getPluginDir()}/${manifest.name}`,
|
|
119
|
+
].join("\n");
|
|
120
|
+
console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ Plugin Info")}\n`);
|
|
121
|
+
console.log(box(lines, `Plugin: ${manifest.name}`));
|
|
122
|
+
console.log("");
|
|
123
|
+
});
|
|
124
|
+
// ─── Main plugin command ──────────────────────────────
|
|
125
|
+
export const pluginCommand = new Command("plugin")
|
|
126
|
+
.description("Manage Stackweld plugins")
|
|
127
|
+
.addCommand(pluginListCommand)
|
|
128
|
+
.addCommand(pluginInstallCommand)
|
|
129
|
+
.addCommand(pluginRemoveCommand)
|
|
130
|
+
.addCommand(pluginInfoCommand);
|
|
131
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld preview <stackId> — Show the docker-compose.yml that would be generated.
|
|
3
|
+
* Does not create any files on disk.
|
|
4
|
+
*/
|
|
5
|
+
import { generateComposePreview } from "@stackweld/core";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { getRulesEngine, getStackEngine } from "../ui/context.js";
|
|
9
|
+
import { box, info, sectionHeader, warning } from "../ui/format.js";
|
|
10
|
+
export const previewCommand = new Command("preview")
|
|
11
|
+
.description("Preview the docker-compose.yml for a saved stack")
|
|
12
|
+
.argument("<id>", "Stack ID")
|
|
13
|
+
.option("--raw", "Output raw YAML without formatting")
|
|
14
|
+
.action((id, opts) => {
|
|
15
|
+
const engine = getStackEngine();
|
|
16
|
+
const rules = getRulesEngine();
|
|
17
|
+
const stack = engine.get(id);
|
|
18
|
+
if (!stack) {
|
|
19
|
+
console.error(chalk.red(`Stack "${id}" not found.`));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
// Resolve full technology objects with stack-level port overrides
|
|
23
|
+
const technologies = stack.technologies
|
|
24
|
+
.map((st) => {
|
|
25
|
+
const tech = rules.getTechnology(st.technologyId);
|
|
26
|
+
if (!tech)
|
|
27
|
+
return null;
|
|
28
|
+
return {
|
|
29
|
+
id: tech.id,
|
|
30
|
+
name: tech.name,
|
|
31
|
+
category: tech.category,
|
|
32
|
+
dockerImage: tech.dockerImage,
|
|
33
|
+
defaultPort: tech.defaultPort,
|
|
34
|
+
envVars: tech.envVars,
|
|
35
|
+
healthCheck: tech.healthCheck,
|
|
36
|
+
port: st.port,
|
|
37
|
+
};
|
|
38
|
+
})
|
|
39
|
+
.filter((t) => t != null);
|
|
40
|
+
const dockerTechs = technologies.filter((t) => t.dockerImage);
|
|
41
|
+
if (dockerTechs.length === 0) {
|
|
42
|
+
console.log(warning("No Docker services in this stack."));
|
|
43
|
+
console.log(chalk.dim(" This stack only contains runtimes/frameworks that run locally."));
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
const result = generateComposePreview(technologies, stack.name);
|
|
47
|
+
if (opts.raw) {
|
|
48
|
+
console.log(result.yaml);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Syntax-highlighted YAML in a box
|
|
52
|
+
const highlighted = highlightYaml(result.yaml);
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log(box(highlighted, `docker-compose.yml ${chalk.dim(stack.name)}`));
|
|
55
|
+
// Service summary
|
|
56
|
+
console.log(sectionHeader("Services:"));
|
|
57
|
+
for (const svc of result.services) {
|
|
58
|
+
const port = result.ports[svc];
|
|
59
|
+
const portStr = port ? chalk.dim(`:${port}`) : "";
|
|
60
|
+
console.log(` ${chalk.green("\u25CF")} ${chalk.cyan(svc)}${portStr}`);
|
|
61
|
+
}
|
|
62
|
+
if (result.volumes.length > 0) {
|
|
63
|
+
console.log(sectionHeader("Volumes:"));
|
|
64
|
+
for (const vol of result.volumes) {
|
|
65
|
+
console.log(` ${chalk.dim("\u25CB")} ${vol}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
console.log("");
|
|
69
|
+
console.log(info("This is a preview. No files were created."));
|
|
70
|
+
console.log(chalk.dim(` Use ${chalk.white(`stackweld scaffold ${id}`)} to generate project files.`));
|
|
71
|
+
console.log("");
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* Basic YAML syntax highlighting for terminal output.
|
|
75
|
+
*/
|
|
76
|
+
function highlightYaml(yaml) {
|
|
77
|
+
return yaml
|
|
78
|
+
.split("\n")
|
|
79
|
+
.map((line) => {
|
|
80
|
+
// Comments
|
|
81
|
+
if (line.trimStart().startsWith("#")) {
|
|
82
|
+
return chalk.dim(line);
|
|
83
|
+
}
|
|
84
|
+
// Key: value lines
|
|
85
|
+
const match = line.match(/^(\s*)([\w.-]+)(:)(.*)/);
|
|
86
|
+
if (match) {
|
|
87
|
+
const [, indent, key, colon, value] = match;
|
|
88
|
+
return `${indent}${chalk.cyan(key)}${chalk.dim(colon)}${chalk.yellow(value)}`;
|
|
89
|
+
}
|
|
90
|
+
// List items
|
|
91
|
+
const listMatch = line.match(/^(\s*)(- )(.*)/);
|
|
92
|
+
if (listMatch) {
|
|
93
|
+
const [, indent, dash, value] = listMatch;
|
|
94
|
+
return `${indent}${chalk.dim(dash)}${chalk.yellow(value)}`;
|
|
95
|
+
}
|
|
96
|
+
return line;
|
|
97
|
+
})
|
|
98
|
+
.join("\n");
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=preview.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld save — Save the current stack state (creates version snapshot).
|
|
3
|
+
*/
|
|
4
|
+
import { input } from "@inquirer/prompts";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { getStackEngine } from "../ui/context.js";
|
|
8
|
+
export const saveCommand = new Command("save")
|
|
9
|
+
.description("Save a version snapshot of a stack")
|
|
10
|
+
.argument("<stack-id>", "Stack ID")
|
|
11
|
+
.option("-m, --message <msg>", "Version changelog message")
|
|
12
|
+
.action(async (stackId, opts) => {
|
|
13
|
+
const engine = getStackEngine();
|
|
14
|
+
const stack = engine.get(stackId);
|
|
15
|
+
if (!stack) {
|
|
16
|
+
console.error(chalk.red(`Stack "${stackId}" not found.`));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const message = opts.message ||
|
|
20
|
+
(await input({
|
|
21
|
+
message: "Changelog message:",
|
|
22
|
+
default: `Snapshot v${stack.version}`,
|
|
23
|
+
}));
|
|
24
|
+
// Trigger an update with no changes to create a new version snapshot
|
|
25
|
+
const result = engine.update(stackId, {});
|
|
26
|
+
if (!result) {
|
|
27
|
+
console.error(chalk.red("Failed to save stack."));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
console.log(chalk.green(`✓ Saved "${stack.name}" as v${result.stack.version}: ${message}`));
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=save.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld scaffold <stack-id> — Generate project files from a saved stack.
|
|
3
|
+
* Generates docker-compose.yml, .env.example, README.md, .gitignore, devcontainer.json.
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
export declare const scaffoldCommand: Command;
|
|
7
|
+
//# sourceMappingURL=scaffold.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld scaffold <stack-id> — Generate project files from a saved stack.
|
|
3
|
+
* Generates docker-compose.yml, .env.example, README.md, .gitignore, devcontainer.json.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { getScaffoldOrchestrator, getStackEngine } from "../ui/context.js";
|
|
10
|
+
export const scaffoldCommand = new Command("scaffold")
|
|
11
|
+
.description("Generate project files from a saved stack")
|
|
12
|
+
.argument("<stack-id>", "Stack ID")
|
|
13
|
+
.option("-p, --path <dir>", "Output directory", ".")
|
|
14
|
+
.option("--dry-run", "Show what would be generated without writing")
|
|
15
|
+
.option("--git", "Initialize Git repository with initial commit")
|
|
16
|
+
.option("--json", "Output generated files as JSON")
|
|
17
|
+
.action((stackId, opts) => {
|
|
18
|
+
const engine = getStackEngine();
|
|
19
|
+
const orchestrator = getScaffoldOrchestrator();
|
|
20
|
+
const stack = engine.get(stackId);
|
|
21
|
+
if (!stack) {
|
|
22
|
+
console.error(chalk.red(`Stack "${stackId}" not found.`));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const output = orchestrator.generate(stack);
|
|
26
|
+
const targetDir = path.resolve(opts.path);
|
|
27
|
+
if (opts.json) {
|
|
28
|
+
console.log(JSON.stringify(output, null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const files = [];
|
|
32
|
+
if (output.dockerCompose) {
|
|
33
|
+
files.push({ path: "docker-compose.yml", content: output.dockerCompose });
|
|
34
|
+
}
|
|
35
|
+
files.push({ path: ".env.example", content: output.envExample });
|
|
36
|
+
files.push({ path: "README.md", content: output.readme });
|
|
37
|
+
files.push({ path: ".gitignore", content: output.gitignore });
|
|
38
|
+
files.push({
|
|
39
|
+
path: ".devcontainer/devcontainer.json",
|
|
40
|
+
content: output.devcontainer,
|
|
41
|
+
});
|
|
42
|
+
files.push({ path: "scripts/dev.sh", content: output.devScript });
|
|
43
|
+
files.push({ path: "scripts/setup.sh", content: output.setupScript });
|
|
44
|
+
files.push({ path: "Makefile", content: output.makefile });
|
|
45
|
+
files.push({
|
|
46
|
+
path: ".vscode/settings.json",
|
|
47
|
+
content: output.vscodeSettings,
|
|
48
|
+
});
|
|
49
|
+
files.push({
|
|
50
|
+
path: ".github/workflows/ci.yml",
|
|
51
|
+
content: output.ciWorkflow,
|
|
52
|
+
});
|
|
53
|
+
// Create all required directories (including empty ones like src/, tests/)
|
|
54
|
+
for (const dir of output.directories) {
|
|
55
|
+
const dirPath = path.join(targetDir, dir);
|
|
56
|
+
if (opts.dryRun) {
|
|
57
|
+
console.log(chalk.dim(`[dry-run] ${dir}/`));
|
|
58
|
+
}
|
|
59
|
+
else if (!fs.existsSync(dirPath)) {
|
|
60
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const filePath = path.join(targetDir, file.path);
|
|
65
|
+
if (opts.dryRun) {
|
|
66
|
+
console.log(chalk.dim(`[dry-run] ${file.path}`));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const dir = path.dirname(filePath);
|
|
70
|
+
if (!fs.existsSync(dir))
|
|
71
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
72
|
+
fs.writeFileSync(filePath, file.content, "utf-8");
|
|
73
|
+
// Make shell scripts executable
|
|
74
|
+
if (file.path.endsWith(".sh")) {
|
|
75
|
+
fs.chmodSync(filePath, 0o755);
|
|
76
|
+
}
|
|
77
|
+
console.log(chalk.green(` ✓ ${file.path}`));
|
|
78
|
+
}
|
|
79
|
+
if (!opts.dryRun) {
|
|
80
|
+
console.log("");
|
|
81
|
+
console.log(chalk.green(`✓ Generated ${files.length} files for "${stack.name}"`));
|
|
82
|
+
// Git init
|
|
83
|
+
if (opts.git) {
|
|
84
|
+
const gitResult = orchestrator.initGit(targetDir, stack);
|
|
85
|
+
if (gitResult.success) {
|
|
86
|
+
console.log(chalk.green(` ✓ ${gitResult.message}`));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log(chalk.yellow(` ⚠ Git: ${gitResult.message}`));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (output.scaffoldCommands.length > 0) {
|
|
93
|
+
console.log(chalk.bold("\nOfficial scaffold commands available:"));
|
|
94
|
+
for (const cmd of output.scaffoldCommands) {
|
|
95
|
+
console.log(` ${chalk.dim("→")} ${cmd.name}: ${chalk.cyan(cmd.command)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld score <techA> [techB] — Show compatibility score between technologies.
|
|
3
|
+
*
|
|
4
|
+
* Two techs: detailed compatibility report.
|
|
5
|
+
* One tech: top 5 best and top 3 worst pairings from the catalog.
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
export declare const scoreCommand: Command;
|
|
9
|
+
//# sourceMappingURL=score.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld score <techA> [techB] — Show compatibility score between technologies.
|
|
3
|
+
*
|
|
4
|
+
* Two techs: detailed compatibility report.
|
|
5
|
+
* One tech: top 5 best and top 3 worst pairings from the catalog.
|
|
6
|
+
*/
|
|
7
|
+
import { scoreCompatibility } from "@stackweld/core";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import { getRulesEngine } from "../ui/context.js";
|
|
11
|
+
import { box } from "../ui/format.js";
|
|
12
|
+
export const scoreCommand = new Command("score")
|
|
13
|
+
.description("Show compatibility score between technologies")
|
|
14
|
+
.argument("<techA>", "First technology ID")
|
|
15
|
+
.argument("[techB]", "Second technology ID (optional)")
|
|
16
|
+
.option("--json", "Output as JSON")
|
|
17
|
+
.action((techA, techB, opts) => {
|
|
18
|
+
const rules = getRulesEngine();
|
|
19
|
+
const a = rules.getTechnology(techA);
|
|
20
|
+
if (!a) {
|
|
21
|
+
console.error(chalk.red(`\u2716 Technology "${techA}" not found in the registry.`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
if (techB) {
|
|
25
|
+
// ── Two-tech mode: detailed compatibility ──
|
|
26
|
+
const b = rules.getTechnology(techB);
|
|
27
|
+
if (!b) {
|
|
28
|
+
console.error(chalk.red(`\u2716 Technology "${techB}" not found in the registry.`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const result = scoreCompatibility(a, b);
|
|
32
|
+
if (opts.json) {
|
|
33
|
+
console.log(JSON.stringify({ techA: a.id, techB: b.id, ...result }, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const barWidth = 20;
|
|
37
|
+
const filled = Math.round((result.score / 100) * barWidth);
|
|
38
|
+
const empty = barWidth - filled;
|
|
39
|
+
const barColor = result.score >= 75 ? chalk.green : result.score >= 45 ? chalk.yellow : chalk.red;
|
|
40
|
+
const bar = barColor("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty));
|
|
41
|
+
const lines = [];
|
|
42
|
+
lines.push("");
|
|
43
|
+
lines.push(` ${bar} ${chalk.bold(String(result.score))}/100`);
|
|
44
|
+
lines.push(` Grade: ${chalk.yellow.bold(result.grade)}`);
|
|
45
|
+
lines.push("");
|
|
46
|
+
if (result.factors.length > 0) {
|
|
47
|
+
lines.push(" Factors:");
|
|
48
|
+
for (const f of result.factors) {
|
|
49
|
+
const icon = f.points >= 0 ? chalk.green("\u2713") : chalk.red("\u2717");
|
|
50
|
+
const sign = f.points >= 0 ? "+" : "";
|
|
51
|
+
const pointStr = f.points >= 0 ? chalk.green(`${sign}${f.points}`) : chalk.red(`${f.points}`);
|
|
52
|
+
lines.push(` ${icon} ${pointStr} ${f.description}`);
|
|
53
|
+
}
|
|
54
|
+
lines.push("");
|
|
55
|
+
}
|
|
56
|
+
lines.push(` ${chalk.dim(`"${result.recommendation}"`)}`);
|
|
57
|
+
lines.push("");
|
|
58
|
+
const title = `Compatibility Score: ${a.name} + ${b.name}`;
|
|
59
|
+
console.log(box(lines.join("\n"), title));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// ── Single-tech mode: best & worst pairings ──
|
|
63
|
+
const allTechs = rules.getAllTechnologies().filter((t) => t.id !== a.id);
|
|
64
|
+
const scored = allTechs.map((t) => ({
|
|
65
|
+
tech: t,
|
|
66
|
+
result: scoreCompatibility(a, t),
|
|
67
|
+
}));
|
|
68
|
+
scored.sort((x, y) => y.result.score - x.result.score);
|
|
69
|
+
if (opts.json) {
|
|
70
|
+
const data = scored.map((s) => ({
|
|
71
|
+
id: s.tech.id,
|
|
72
|
+
name: s.tech.name,
|
|
73
|
+
score: s.result.score,
|
|
74
|
+
grade: s.result.grade,
|
|
75
|
+
}));
|
|
76
|
+
console.log(JSON.stringify({ technology: a.id, pairings: data }, null, 2));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
console.log("");
|
|
80
|
+
console.log(chalk.bold.cyan(` ${a.name}`) + chalk.dim(` (${a.id})`));
|
|
81
|
+
console.log(chalk.dim(` ${a.category} — ${a.description}`));
|
|
82
|
+
console.log("");
|
|
83
|
+
// Top 5 best
|
|
84
|
+
console.log(chalk.green.bold(" Top 5 Best Pairings:"));
|
|
85
|
+
console.log("");
|
|
86
|
+
const best = scored.slice(0, 5);
|
|
87
|
+
for (const item of best) {
|
|
88
|
+
const barWidth = 15;
|
|
89
|
+
const filled = Math.round((item.result.score / 100) * barWidth);
|
|
90
|
+
const empty = barWidth - filled;
|
|
91
|
+
const bar = chalk.green("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty));
|
|
92
|
+
console.log(` ${bar} ${chalk.bold(String(item.result.score).padStart(3))}/100 ` +
|
|
93
|
+
`${chalk.yellow(item.result.grade)} ${chalk.cyan(item.tech.name)} ${chalk.dim(`(${item.tech.category})`)}`);
|
|
94
|
+
}
|
|
95
|
+
console.log("");
|
|
96
|
+
// Top 3 worst
|
|
97
|
+
console.log(chalk.red.bold(" Top 3 Worst Pairings:"));
|
|
98
|
+
console.log("");
|
|
99
|
+
const worst = scored.slice(-3).reverse();
|
|
100
|
+
for (const item of worst) {
|
|
101
|
+
const barWidth = 15;
|
|
102
|
+
const filled = Math.round((item.result.score / 100) * barWidth);
|
|
103
|
+
const empty = barWidth - filled;
|
|
104
|
+
const bar = chalk.red("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty));
|
|
105
|
+
console.log(` ${bar} ${chalk.bold(String(item.result.score).padStart(3))}/100 ` +
|
|
106
|
+
`${chalk.yellow(item.result.grade)} ${chalk.cyan(item.tech.name)} ${chalk.dim(`(${item.tech.category})`)}`);
|
|
107
|
+
}
|
|
108
|
+
console.log("");
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
//# sourceMappingURL=score.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld share <stackId> — Generate a shareable URL for a saved stack.
|
|
3
|
+
* stackweld import-url <url> — Import a stack from a share URL.
|
|
4
|
+
*
|
|
5
|
+
* Stack data is encoded in the URL hash — no server needed.
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
export declare const shareCommand: Command;
|
|
9
|
+
export declare const importUrlCommand: Command;
|
|
10
|
+
//# sourceMappingURL=share.d.ts.map
|