@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,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld import <file> — Import a stack definition from YAML or JSON.
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { parse as yamlParse } from "yaml";
|
|
8
|
+
import { getStackEngine } from "../ui/context.js";
|
|
9
|
+
import { formatStackRow, formatValidation } from "../ui/format.js";
|
|
10
|
+
export const importCommand = new Command("import")
|
|
11
|
+
.description("Import a stack definition from a YAML or JSON file")
|
|
12
|
+
.argument("<file>", "Path to the YAML or JSON file")
|
|
13
|
+
.option("--json", "Output result as JSON")
|
|
14
|
+
.action((file, opts) => {
|
|
15
|
+
if (!fs.existsSync(file)) {
|
|
16
|
+
console.error(chalk.red(`File not found: ${file}`));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
20
|
+
let data;
|
|
21
|
+
// Detect format
|
|
22
|
+
if (file.endsWith(".json")) {
|
|
23
|
+
try {
|
|
24
|
+
data = JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
console.error(chalk.red("Invalid JSON file."));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Try YAML (also handles JSON since JSON is valid YAML)
|
|
33
|
+
try {
|
|
34
|
+
data = yamlParse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
console.error(chalk.red("Invalid YAML/JSON file."));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!data.name || !data.technologies || !Array.isArray(data.technologies)) {
|
|
42
|
+
console.error(chalk.red("Invalid stack file. Required: name, technologies array."));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
const engine = getStackEngine();
|
|
46
|
+
const technologies = data.technologies.map((t) => ({
|
|
47
|
+
technologyId: t.id,
|
|
48
|
+
version: t.version,
|
|
49
|
+
port: t.port,
|
|
50
|
+
}));
|
|
51
|
+
const { stack, validation } = engine.create({
|
|
52
|
+
name: data.name,
|
|
53
|
+
description: data.description,
|
|
54
|
+
profile: data.profile || "standard",
|
|
55
|
+
technologies,
|
|
56
|
+
tags: data.tags,
|
|
57
|
+
});
|
|
58
|
+
console.log(formatValidation(validation));
|
|
59
|
+
console.log("");
|
|
60
|
+
if (opts.json) {
|
|
61
|
+
console.log(JSON.stringify({ stack, validation }, null, 2));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log(formatStackRow(stack));
|
|
65
|
+
console.log(chalk.green(`\n✓ Imported "${stack.name}" successfully`));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=import-stack.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld info <id> — Show detailed information about a stack or technology.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { getRulesEngine, getStackEngine } from "../ui/context.js";
|
|
7
|
+
import { formatJson, formatStackRow, formatTechnology } from "../ui/format.js";
|
|
8
|
+
export const infoCommand = new Command("info")
|
|
9
|
+
.description("Show details about a stack or technology")
|
|
10
|
+
.argument("<id>", "Stack ID or technology ID")
|
|
11
|
+
.option("--json", "Output as JSON")
|
|
12
|
+
.action((id, opts) => {
|
|
13
|
+
const engine = getStackEngine();
|
|
14
|
+
const rules = getRulesEngine();
|
|
15
|
+
// Try stack first
|
|
16
|
+
const stack = engine.get(id);
|
|
17
|
+
if (stack) {
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(formatJson(stack));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log(formatStackRow(stack));
|
|
23
|
+
// Show individual tech details
|
|
24
|
+
console.log(chalk.bold("\nTechnologies:"));
|
|
25
|
+
for (const t of stack.technologies) {
|
|
26
|
+
const tech = rules.getTechnology(t.technologyId);
|
|
27
|
+
if (tech) {
|
|
28
|
+
console.log(` ${chalk.cyan(tech.name)} ${chalk.dim(`v${t.version}`)} ${t.port ? chalk.dim(`:${t.port}`) : ""}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Show version history
|
|
32
|
+
const history = engine.getVersionHistory(stack.id);
|
|
33
|
+
if (history.length > 1) {
|
|
34
|
+
console.log(chalk.bold("\nVersion history:"));
|
|
35
|
+
for (const v of history.slice(0, 5)) {
|
|
36
|
+
console.log(` ${chalk.dim(`v${v.version}`)} ${chalk.dim(v.timestamp)} ${v.changelog}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Try technology
|
|
43
|
+
const tech = rules.getTechnology(id);
|
|
44
|
+
if (tech) {
|
|
45
|
+
if (opts.json) {
|
|
46
|
+
console.log(formatJson(tech));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.log(formatTechnology(tech));
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
console.error(chalk.red(`"${id}" not found as stack or technology.`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=info.js.map
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld init — Initialize Stackweld in the current directory or interactively create a new stack.
|
|
3
|
+
*/
|
|
4
|
+
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
5
|
+
import { getAllTemplates } from "@stackweld/templates";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { getRulesEngine, getStackEngine } from "../ui/context.js";
|
|
9
|
+
import { formatStackSummary, formatValidation, gradientHeader, nextSteps, stepIndicator, warning, } from "../ui/format.js";
|
|
10
|
+
export const initCommand = new Command("init")
|
|
11
|
+
.description("Create a new stack interactively")
|
|
12
|
+
.option("--template <id>", "Start from a template")
|
|
13
|
+
.option("--json", "Output result as JSON")
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ New Stack Wizard")}\n`);
|
|
16
|
+
const rules = getRulesEngine();
|
|
17
|
+
const engine = getStackEngine();
|
|
18
|
+
// ── Step 1: Choose mode ──
|
|
19
|
+
console.log(stepIndicator(1, 5, "Choose how to start"));
|
|
20
|
+
const mode = opts.template
|
|
21
|
+
? "template"
|
|
22
|
+
: await select({
|
|
23
|
+
message: "How do you want to start?",
|
|
24
|
+
choices: [
|
|
25
|
+
{ name: "From scratch — pick technologies one by one", value: "scratch" },
|
|
26
|
+
{ name: "From a template — use a pre-built stack", value: "template" },
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
let technologies = [];
|
|
30
|
+
let profile = "standard";
|
|
31
|
+
let stackName = "";
|
|
32
|
+
if (mode === "template") {
|
|
33
|
+
// ── Step 2: Select template ──
|
|
34
|
+
console.log(`\n${stepIndicator(2, 5, "Select template")}`);
|
|
35
|
+
const templates = getAllTemplates();
|
|
36
|
+
if (templates.length === 0) {
|
|
37
|
+
console.log(warning("No templates available. Run from scratch instead."));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const templateId = opts.template ||
|
|
41
|
+
(await select({
|
|
42
|
+
message: "Choose a template:",
|
|
43
|
+
choices: templates.map((t) => ({
|
|
44
|
+
name: `${chalk.cyan(t.name)} ${chalk.dim("—")} ${t.description}`,
|
|
45
|
+
value: t.id,
|
|
46
|
+
})),
|
|
47
|
+
}));
|
|
48
|
+
const template = templates.find((t) => t.id === templateId);
|
|
49
|
+
if (!template) {
|
|
50
|
+
console.error(chalk.red(`\u2716 Template "${templateId}" not found.`));
|
|
51
|
+
console.error(chalk.dim(` Available templates: ${templates.map((t) => t.id).join(", ")}`));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// ── Step 3: Name ──
|
|
55
|
+
console.log(`\n${stepIndicator(3, 5, "Name your project")}`);
|
|
56
|
+
stackName = await input({
|
|
57
|
+
message: "Project name:",
|
|
58
|
+
default: template.variables.projectName || "my-project",
|
|
59
|
+
validate: (v) => (v.trim().length > 0 ? true : "Name cannot be empty"),
|
|
60
|
+
});
|
|
61
|
+
technologies = template.technologyIds.map((tid) => {
|
|
62
|
+
const tech = rules.getTechnology(tid);
|
|
63
|
+
return {
|
|
64
|
+
technologyId: tid,
|
|
65
|
+
version: tech?.defaultVersion || "latest",
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
profile = template.profile;
|
|
69
|
+
// Steps 4-5 skipped for template mode
|
|
70
|
+
console.log(`\n${stepIndicator(4, 5, chalk.dim("Profile: ") + profile)}`);
|
|
71
|
+
console.log(`${stepIndicator(5, 5, `${chalk.dim("Technologies: ") + technologies.length} from template`)}`);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// ── Step 2: Name ──
|
|
75
|
+
console.log(`\n${stepIndicator(2, 5, "Name your stack")}`);
|
|
76
|
+
stackName = await input({
|
|
77
|
+
message: "Stack name:",
|
|
78
|
+
default: "my-stack",
|
|
79
|
+
validate: (v) => (v.trim().length > 0 ? true : "Name cannot be empty"),
|
|
80
|
+
});
|
|
81
|
+
// ── Step 3: Profile ──
|
|
82
|
+
console.log(`\n${stepIndicator(3, 5, "Choose a project profile")}`);
|
|
83
|
+
profile = (await select({
|
|
84
|
+
message: "Project profile:",
|
|
85
|
+
choices: [
|
|
86
|
+
{
|
|
87
|
+
name: `${chalk.cyan("Rapid")} ${chalk.dim("— Quick prototyping, minimal config")}`,
|
|
88
|
+
value: "rapid",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: `${chalk.cyan("Standard")} ${chalk.dim("— Balanced defaults for most projects")}`,
|
|
92
|
+
value: "standard",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: `${chalk.cyan("Production")} ${chalk.dim("— Battle-tested, monitoring included")}`,
|
|
96
|
+
value: "production",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: `${chalk.cyan("Enterprise")} ${chalk.dim("— Full compliance, audit, security")}`,
|
|
100
|
+
value: "enterprise",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: `${chalk.cyan("Lightweight")}${chalk.dim("— Minimal footprint")}`,
|
|
104
|
+
value: "lightweight",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
}));
|
|
108
|
+
// ── Step 4: Technologies ──
|
|
109
|
+
console.log(`\n${stepIndicator(4, 5, "Select technologies by category")}`);
|
|
110
|
+
const categories = [
|
|
111
|
+
"runtime",
|
|
112
|
+
"frontend",
|
|
113
|
+
"backend",
|
|
114
|
+
"database",
|
|
115
|
+
"orm",
|
|
116
|
+
"auth",
|
|
117
|
+
"styling",
|
|
118
|
+
"service",
|
|
119
|
+
"devops",
|
|
120
|
+
];
|
|
121
|
+
for (const category of categories) {
|
|
122
|
+
const available = rules.getByCategory(category);
|
|
123
|
+
if (available.length === 0)
|
|
124
|
+
continue;
|
|
125
|
+
const selected = await checkbox({
|
|
126
|
+
message: `${chalk.cyan(category)} technologies:`,
|
|
127
|
+
choices: available.map((t) => ({
|
|
128
|
+
name: `${t.name} ${chalk.dim(`(${t.defaultVersion})`)}`,
|
|
129
|
+
value: t.id,
|
|
130
|
+
})),
|
|
131
|
+
});
|
|
132
|
+
for (const id of selected) {
|
|
133
|
+
const tech = rules.getTechnology(id);
|
|
134
|
+
if (tech) {
|
|
135
|
+
technologies.push({
|
|
136
|
+
technologyId: id,
|
|
137
|
+
version: tech.defaultVersion,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// ── Step 5: Confirm ──
|
|
143
|
+
console.log(`\n${stepIndicator(5, 5, "Review and confirm")}`);
|
|
144
|
+
}
|
|
145
|
+
if (technologies.length === 0) {
|
|
146
|
+
console.log(warning("No technologies selected. Aborting."));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Show summary before creating
|
|
150
|
+
console.log("");
|
|
151
|
+
console.log(chalk.bold(" Summary:"));
|
|
152
|
+
console.log(` ${chalk.dim("Name:")} ${chalk.cyan(stackName)}`);
|
|
153
|
+
console.log(` ${chalk.dim("Profile:")} ${profile}`);
|
|
154
|
+
console.log(` ${chalk.dim("Techs:")} ${technologies.map((t) => t.technologyId).join(", ")}`);
|
|
155
|
+
console.log("");
|
|
156
|
+
if (mode !== "template") {
|
|
157
|
+
const proceed = await confirm({
|
|
158
|
+
message: "Create this stack?",
|
|
159
|
+
default: true,
|
|
160
|
+
});
|
|
161
|
+
if (!proceed) {
|
|
162
|
+
console.log(chalk.dim(" Cancelled."));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const { stack, validation } = engine.create({
|
|
167
|
+
name: stackName,
|
|
168
|
+
profile,
|
|
169
|
+
technologies,
|
|
170
|
+
});
|
|
171
|
+
console.log("");
|
|
172
|
+
console.log(formatValidation(validation));
|
|
173
|
+
console.log("");
|
|
174
|
+
if (opts.json) {
|
|
175
|
+
console.log(JSON.stringify({ stack, validation }, null, 2));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.log(formatStackSummary(stack));
|
|
179
|
+
console.log(nextSteps([
|
|
180
|
+
`stackweld scaffold ${stack.id} --path .`,
|
|
181
|
+
`stackweld generate --name ${stackName} --path . --techs ${technologies.map((t) => t.technologyId).join(",")}`,
|
|
182
|
+
`stackweld info ${stack.id}`,
|
|
183
|
+
]));
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld learn <technology> — Show learning resources for a technology.
|
|
3
|
+
*/
|
|
4
|
+
import { getLearningResources } from "@stackweld/registry";
|
|
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_ICONS = {
|
|
10
|
+
beginner: "\u{1F4D6}", // open book
|
|
11
|
+
intermediate: "\u{1F4DA}", // books
|
|
12
|
+
advanced: "\u{1F393}", // graduation cap
|
|
13
|
+
};
|
|
14
|
+
const DIFFICULTY_ORDER = {
|
|
15
|
+
beginner: 0,
|
|
16
|
+
intermediate: 1,
|
|
17
|
+
advanced: 2,
|
|
18
|
+
};
|
|
19
|
+
const DIFFICULTY_LABELS = {
|
|
20
|
+
beginner: "Beginner",
|
|
21
|
+
intermediate: "Intermediate",
|
|
22
|
+
advanced: "Advanced",
|
|
23
|
+
};
|
|
24
|
+
export const learnCommand = new Command("learn")
|
|
25
|
+
.description("Show learning resources for a technology")
|
|
26
|
+
.argument("<technology>", "Technology ID (e.g. nextjs, react, docker)")
|
|
27
|
+
.option("--json", "Output as JSON")
|
|
28
|
+
.action((technologyId, opts) => {
|
|
29
|
+
const rules = getRulesEngine();
|
|
30
|
+
const tech = rules.getTechnology(technologyId);
|
|
31
|
+
const resources = getLearningResources(technologyId);
|
|
32
|
+
const displayName = tech?.name ?? technologyId;
|
|
33
|
+
if (!resources || resources.length === 0) {
|
|
34
|
+
if (opts.json) {
|
|
35
|
+
console.log(JSON.stringify({ technology: technologyId, resources: [] }, null, 2));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log(chalk.yellow(` No learning resources found for "${technologyId}".`));
|
|
40
|
+
console.log("");
|
|
41
|
+
console.log(chalk.dim(" Try searching the official website or documentation."));
|
|
42
|
+
if (tech?.website) {
|
|
43
|
+
console.log(chalk.dim(` Website: ${chalk.cyan(tech.website)}`));
|
|
44
|
+
}
|
|
45
|
+
console.log("");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
console.log(JSON.stringify({ technology: technologyId, name: displayName, resources }, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Group resources by difficulty
|
|
53
|
+
const grouped = {};
|
|
54
|
+
for (const resource of resources) {
|
|
55
|
+
if (!grouped[resource.difficulty]) {
|
|
56
|
+
grouped[resource.difficulty] = [];
|
|
57
|
+
}
|
|
58
|
+
grouped[resource.difficulty].push(resource);
|
|
59
|
+
}
|
|
60
|
+
const lines = [];
|
|
61
|
+
lines.push("");
|
|
62
|
+
let counter = 1;
|
|
63
|
+
const difficulties = Object.keys(grouped).sort((a, b) => (DIFFICULTY_ORDER[a] ?? 99) - (DIFFICULTY_ORDER[b] ?? 99));
|
|
64
|
+
for (const difficulty of difficulties) {
|
|
65
|
+
const icon = DIFFICULTY_ICONS[difficulty] ?? "";
|
|
66
|
+
const label = DIFFICULTY_LABELS[difficulty] ?? difficulty;
|
|
67
|
+
lines.push(` ${icon} ${chalk.bold(label)}`);
|
|
68
|
+
for (const resource of grouped[difficulty]) {
|
|
69
|
+
lines.push(` ${chalk.white(`${counter}.`)} ${resource.title}`);
|
|
70
|
+
lines.push(` ${chalk.cyan(resource.url)}`);
|
|
71
|
+
counter++;
|
|
72
|
+
}
|
|
73
|
+
lines.push("");
|
|
74
|
+
}
|
|
75
|
+
// Check for missing difficulty levels and show placeholder
|
|
76
|
+
for (const diff of ["beginner", "intermediate", "advanced"]) {
|
|
77
|
+
if (!grouped[diff]) {
|
|
78
|
+
const icon = DIFFICULTY_ICONS[diff] ?? "";
|
|
79
|
+
const label = DIFFICULTY_LABELS[diff] ?? diff;
|
|
80
|
+
lines.push(` ${icon} ${chalk.bold(label)}`);
|
|
81
|
+
lines.push(chalk.dim(` (No ${diff} resources yet)`));
|
|
82
|
+
lines.push("");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
lines.push(` ${chalk.dim("Tip: Start with #1, then build a")}`);
|
|
86
|
+
lines.push(` ${chalk.dim("project before moving to intermediate.")}`);
|
|
87
|
+
lines.push("");
|
|
88
|
+
console.log(box(lines.join("\n"), `Learning Path: ${displayName}`));
|
|
89
|
+
console.log("");
|
|
90
|
+
});
|
|
91
|
+
//# sourceMappingURL=learn.js.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld lint — Validate a stack against team standards (.stackweldrc).
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { detectStack, lintStack, loadStandards } from "@stackweld/core";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { getStackEngine } from "../ui/context.js";
|
|
10
|
+
import { box, formatJson, gradientHeader } from "../ui/format.js";
|
|
11
|
+
function buildStackFromDetected(detected) {
|
|
12
|
+
return {
|
|
13
|
+
name: "detected",
|
|
14
|
+
profile: "standard",
|
|
15
|
+
technologies: detected.technologies.map((t) => ({ technologyId: t.id })),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export const lintCommand = new Command("lint")
|
|
19
|
+
.description("Validate a stack against team standards (.stackweldrc)")
|
|
20
|
+
.option("-c, --config <path>", "Path to standards config file")
|
|
21
|
+
.option("-s, --stack <id>", "Lint a saved stack by ID")
|
|
22
|
+
.option("--json", "Output as JSON")
|
|
23
|
+
.action((opts) => {
|
|
24
|
+
// ── Load standards ──
|
|
25
|
+
const configPath = opts.config
|
|
26
|
+
? path.resolve(opts.config)
|
|
27
|
+
: path.resolve(process.cwd(), ".stackweldrc");
|
|
28
|
+
let standards = null;
|
|
29
|
+
if (opts.config) {
|
|
30
|
+
// Explicit config path
|
|
31
|
+
if (!fs.existsSync(configPath)) {
|
|
32
|
+
console.error(chalk.red(`\u2716 Config file not found: ${configPath}`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
standards = loadStandards(path.dirname(configPath));
|
|
36
|
+
if (!standards) {
|
|
37
|
+
// Try reading directly if the dirname resolution didn't work
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
40
|
+
standards = JSON.parse(content);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
console.error(chalk.red(`\u2716 Could not parse config: ${configPath}`));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
standards = loadStandards(process.cwd());
|
|
50
|
+
}
|
|
51
|
+
if (!standards) {
|
|
52
|
+
console.error(chalk.red("\u2716 No .stackweldrc found in current directory."));
|
|
53
|
+
console.error(chalk.dim(" Create one or use --config <path>"));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
// ── Resolve stack ──
|
|
57
|
+
let stackLike;
|
|
58
|
+
if (opts.stack) {
|
|
59
|
+
const engine = getStackEngine();
|
|
60
|
+
const saved = engine.get(opts.stack);
|
|
61
|
+
if (!saved) {
|
|
62
|
+
console.error(chalk.red(`\u2716 Stack "${opts.stack}" not found.`));
|
|
63
|
+
console.error(chalk.dim(" Run: stackweld list"));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
stackLike = saved;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Detect from current directory
|
|
70
|
+
const detected = detectStack(process.cwd());
|
|
71
|
+
if (detected.technologies.length === 0) {
|
|
72
|
+
console.error(chalk.red("\u2716 Could not detect any technologies in the current directory."));
|
|
73
|
+
console.error(chalk.dim(" Use --stack <id> to lint a saved stack instead."));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
stackLike = buildStackFromDetected(detected);
|
|
77
|
+
}
|
|
78
|
+
// ── Run lint ──
|
|
79
|
+
// Cast to StackDefinition shape for the linter
|
|
80
|
+
const result = lintStack(stackLike, standards);
|
|
81
|
+
if (opts.json) {
|
|
82
|
+
console.log(formatJson({ standards, result }));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// ── Format output ──
|
|
86
|
+
const teamName = standards.team || "Team Standards";
|
|
87
|
+
const lines = [];
|
|
88
|
+
lines.push("");
|
|
89
|
+
let passCount = 0;
|
|
90
|
+
let failCount = 0;
|
|
91
|
+
// Required technologies
|
|
92
|
+
if (standards.requiredTechnologies) {
|
|
93
|
+
const techIds = new Set(stackLike.technologies.map((t) => t.technologyId));
|
|
94
|
+
for (const req of standards.requiredTechnologies) {
|
|
95
|
+
if (techIds.has(req)) {
|
|
96
|
+
lines.push(` ${chalk.green("\u2713")} Required: ${req}`);
|
|
97
|
+
passCount++;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
lines.push(` ${chalk.red("\u2717")} Required: ${req} ${chalk.red("missing")}`);
|
|
101
|
+
failCount++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Blocked technologies
|
|
106
|
+
if (standards.blockedTechnologies) {
|
|
107
|
+
const techIds = new Set(stackLike.technologies.map((t) => t.technologyId));
|
|
108
|
+
for (const blocked of standards.blockedTechnologies) {
|
|
109
|
+
if (techIds.has(blocked)) {
|
|
110
|
+
lines.push(` ${chalk.red("\u2717")} Blocked: ${blocked} found ${chalk.red("\u2014 remove it")}`);
|
|
111
|
+
failCount++;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
lines.push(` ${chalk.green("\u2713")} Blocked: ${blocked} ${chalk.dim("(not present)")}`);
|
|
115
|
+
passCount++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Min profile
|
|
120
|
+
if (standards.minProfile) {
|
|
121
|
+
const profileViolation = result.warnings.find((w) => w.rule === "minProfile");
|
|
122
|
+
if (profileViolation) {
|
|
123
|
+
lines.push(` ${chalk.yellow("\u26A0")} Profile: ${stackLike.profile} < ${standards.minProfile}`);
|
|
124
|
+
failCount++;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
lines.push(` ${chalk.green("\u2713")} Profile: ${stackLike.profile} \u2265 ${standards.minProfile}`);
|
|
128
|
+
passCount++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Require Docker
|
|
132
|
+
if (standards.requireDocker) {
|
|
133
|
+
const dockerViolation = result.violations.find((v) => v.rule === "requireDocker");
|
|
134
|
+
if (dockerViolation) {
|
|
135
|
+
lines.push(` ${chalk.red("\u2717")} Docker required but missing`);
|
|
136
|
+
failCount++;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
lines.push(` ${chalk.green("\u2713")} Docker present`);
|
|
140
|
+
passCount++;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Require tests
|
|
144
|
+
if (standards.requireTests) {
|
|
145
|
+
const testWarning = result.warnings.find((w) => w.rule === "requireTests");
|
|
146
|
+
if (testWarning) {
|
|
147
|
+
lines.push(` ${chalk.yellow("\u26A0")} No testing framework found`);
|
|
148
|
+
failCount++;
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
lines.push(` ${chalk.green("\u2713")} Tests present`);
|
|
152
|
+
passCount++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Require TypeScript
|
|
156
|
+
if (standards.requireTypeScript) {
|
|
157
|
+
const tsViolation = result.violations.find((v) => v.rule === "requireTypeScript");
|
|
158
|
+
if (tsViolation) {
|
|
159
|
+
lines.push(` ${chalk.red("\u2717")} TypeScript required but missing`);
|
|
160
|
+
failCount++;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
lines.push(` ${chalk.green("\u2713")} TypeScript present`);
|
|
164
|
+
passCount++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Custom rules
|
|
168
|
+
if (standards.customRules) {
|
|
169
|
+
for (const rule of standards.customRules) {
|
|
170
|
+
const violation = result.violations.find((v) => v.rule === `custom:${rule.name}`);
|
|
171
|
+
if (violation) {
|
|
172
|
+
lines.push(` ${chalk.red("\u2717")} ${rule.message}`);
|
|
173
|
+
failCount++;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
lines.push(` ${chalk.green("\u2713")} Custom: ${rule.name}`);
|
|
177
|
+
passCount++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
lines.push("");
|
|
182
|
+
lines.push(` ${chalk.green(`${String(passCount)} passed`)} ${chalk.dim("\u00B7")} ` +
|
|
183
|
+
`${failCount > 0 ? chalk.red(`${String(failCount)} violation(s)`) : chalk.dim("0 violations")}`);
|
|
184
|
+
lines.push(` Status: ${result.passed ? chalk.green.bold("PASSED") : chalk.red.bold("FAILED")}`);
|
|
185
|
+
lines.push("");
|
|
186
|
+
console.log(`\n ${gradientHeader("Stackweld")} ${chalk.dim("/ Lint")}\n`);
|
|
187
|
+
console.log(box(lines.join("\n"), `Stack Lint: ${teamName}`));
|
|
188
|
+
console.log("");
|
|
189
|
+
if (!result.passed) {
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
//# sourceMappingURL=lint.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stackweld list — List saved stacks.
|
|
3
|
+
*/
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { getStackEngine } from "../ui/context.js";
|
|
7
|
+
import { emptyState, formatJson, formatStackTable } from "../ui/format.js";
|
|
8
|
+
export const listCommand = new Command("list")
|
|
9
|
+
.aliases(["ls"])
|
|
10
|
+
.description("List all saved stacks")
|
|
11
|
+
.option("--json", "Output as JSON")
|
|
12
|
+
.action((opts) => {
|
|
13
|
+
const engine = getStackEngine();
|
|
14
|
+
const stacks = engine.list();
|
|
15
|
+
if (stacks.length === 0) {
|
|
16
|
+
console.log(emptyState("No stacks saved yet.", "Run `stackweld init` to create one."));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (opts.json) {
|
|
20
|
+
console.log(formatJson(stacks));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log(chalk.bold(`\n ${stacks.length} stack(s)\n`));
|
|
24
|
+
console.log(formatStackTable(stacks));
|
|
25
|
+
console.log("");
|
|
26
|
+
});
|
|
27
|
+
//# sourceMappingURL=list.js.map
|