stackloom-cli 1.0.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/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/cli.js +306 -0
- package/branding.json +8 -0
- package/package.json +72 -0
- package/src/__tests__/cli-smoke.test.js +46 -0
- package/src/blueprint/__tests__/blueprint.test.js +116 -0
- package/src/blueprint/blueprint.js +181 -0
- package/src/blueprint/default.blueprint.json +78 -0
- package/src/blueprint/index.js +10 -0
- package/src/blueprint/loader.js +101 -0
- package/src/blueprint/schema-kit.js +161 -0
- package/src/blueprint/schema.js +78 -0
- package/src/branding/__tests__/branding.test.js +49 -0
- package/src/branding/index.js +48 -0
- package/src/commands/__tests__/commands.test.js +83 -0
- package/src/commands/check.js +71 -0
- package/src/commands/cleanup.js +347 -0
- package/src/commands/customize.js +263 -0
- package/src/commands/doctor.js +84 -0
- package/src/commands/env.js +75 -0
- package/src/commands/finalize.js +68 -0
- package/src/commands/generate/ci-cd.js +378 -0
- package/src/commands/generate/deploy-advanced.js +253 -0
- package/src/commands/generate/deploy.js +99 -0
- package/src/commands/generate/env.template.js +221 -0
- package/src/commands/generate/index.js +7 -0
- package/src/commands/generate/module.js +836 -0
- package/src/commands/generate/page.js +1415 -0
- package/src/commands/generate/test-scaffold.js +279 -0
- package/src/commands/generate/theme.js +67 -0
- package/src/commands/generate-resource.js +133 -0
- package/src/commands/index.js +9 -0
- package/src/commands/init.js +350 -0
- package/src/commands/make/resource.js +298 -0
- package/src/commands/preset.js +57 -0
- package/src/commands/remove.js +170 -0
- package/src/commands/rename.js +54 -0
- package/src/commands/rollback.js +90 -0
- package/src/commands/wizard.js +303 -0
- package/src/core/__tests__/generator.test.js +67 -0
- package/src/core/__tests__/marker-strategy.test.js +57 -0
- package/src/core/__tests__/resource-definition.test.js +32 -0
- package/src/core/generator.js +542 -0
- package/src/core/marker-strategy.js +138 -0
- package/src/core/resource-definition.js +346 -0
- package/src/core/state-tracker.js +67 -0
- package/src/core/template-loader.js +163 -0
- package/src/engine/__tests__/engine.test.js +306 -0
- package/src/engine/index.js +21 -0
- package/src/engine/injector.js +198 -0
- package/src/engine/pipeline.js +138 -0
- package/src/engine/transaction.js +105 -0
- package/src/engine/validator.js +190 -0
- package/src/index.js +4 -0
- package/src/recipes/__tests__/recipe.test.js +128 -0
- package/src/recipes/builtin/module.json +22 -0
- package/src/recipes/builtin/page.json +21 -0
- package/src/recipes/builtin/resource.json +35 -0
- package/src/recipes/condition.js +147 -0
- package/src/recipes/index.js +11 -0
- package/src/recipes/loader.js +95 -0
- package/src/recipes/recipe.js +89 -0
- package/src/recipes/schema.js +47 -0
- package/src/schemas/__tests__/schemas.test.js +67 -0
- package/src/schemas/index.js +18 -0
- package/src/schemas/options.js +38 -0
- package/src/schemas/resource.js +112 -0
- package/src/services/__tests__/reporter.test.js +98 -0
- package/src/services/clock.js +31 -0
- package/src/services/index.js +43 -0
- package/src/services/reporter.js +136 -0
- package/src/templates/resource/api.js.ejs +39 -0
- package/src/templates/resource/components/form.jsx.ejs +81 -0
- package/src/templates/resource/components/table.jsx.ejs +68 -0
- package/src/templates/resource/controller.js.ejs +154 -0
- package/src/templates/resource/hooks.js.ejs +46 -0
- package/src/templates/resource/model.js.ejs +64 -0
- package/src/templates/resource/page-detail.jsx.ejs +55 -0
- package/src/templates/resource/page-form.jsx.ejs +30 -0
- package/src/templates/resource/page-inline.jsx.ejs +74 -0
- package/src/templates/resource/page-modal.jsx.ejs +98 -0
- package/src/templates/resource/page-page.jsx.ejs +99 -0
- package/src/templates/resource/page-sidepanel.jsx.ejs +100 -0
- package/src/templates/resource/routes.js.ejs +35 -0
- package/src/templates/resource/service.js.ejs +132 -0
- package/src/templates/resource/test.ejs +71 -0
- package/src/templates/resource/types.ts.ejs +17 -0
- package/src/templates/resource/validator.js.ejs +26 -0
- package/src/templates/snippets/lazy-import.ejs +1 -0
- package/src/templates/snippets/nav-entry.ejs +1 -0
- package/src/templates/snippets/route-entry.ejs +5 -0
- package/src/templates/snippets/route-mount.ejs +1 -0
- package/src/utils/fieldValidators.js +371 -0
- package/src/utils/logging/logger.js +47 -0
- package/src/utils/namingUtils.js +38 -0
- package/src/utils/sanitize.js +200 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Niyo Abou Sharif
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# stackloom
|
|
2
|
+
|
|
3
|
+
> Weave production-ready full-stack apps from a single command.
|
|
4
|
+
|
|
5
|
+
`stackloom` is a recipe-driven, transactional code-generation CLI. It scaffolds
|
|
6
|
+
a complete MERN application and then keeps extending it — full-stack resources,
|
|
7
|
+
admin pages, deploy configs — without ever leaving a half-written file behind.
|
|
8
|
+
|
|
9
|
+
The CLI command is **`loom`**. It is **rebrandable**: run `loom rename <name>`
|
|
10
|
+
to make the whole tool your own.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# one-off
|
|
16
|
+
npx stackloom new my-app
|
|
17
|
+
|
|
18
|
+
# or global
|
|
19
|
+
pnpm add -g stackloom
|
|
20
|
+
loom new my-app
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How it works
|
|
24
|
+
|
|
25
|
+
The CLI is a generic engine — it hardcodes nothing about MERN. Three layers:
|
|
26
|
+
|
|
27
|
+
| Layer | Lives in | Answers |
|
|
28
|
+
|-------|----------|---------|
|
|
29
|
+
| **Blueprint** | a project's `.loom/blueprint.json` | *where* things go — directory roots, path templates, injection anchors |
|
|
30
|
+
| **Recipe** | `src/recipes/builtin/*.json` | *what* gets generated — files, injections, dependencies, gated by `when` conditions |
|
|
31
|
+
| **Engine** | `src/engine/` | *how* — a transactional pipeline: `plan → render → inject → validate → commit` |
|
|
32
|
+
|
|
33
|
+
Generation is **all-or-nothing**: every file is rendered into a staging
|
|
34
|
+
transaction, syntax-validated, and only a fully-valid set is committed. A
|
|
35
|
+
broken file is never written; on any failure the whole change set rolls back.
|
|
36
|
+
|
|
37
|
+
Adding support for another stack (Next.js, PERN, …) is a new `blueprint.json` —
|
|
38
|
+
not an engine change.
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
| Command | What it does |
|
|
43
|
+
|---------|--------------|
|
|
44
|
+
| `loom new [name]` | Create a new project from the starter template |
|
|
45
|
+
| `loom generate resource <Name>` | **Unified, engine-backed generator** — full-stack CRUD resource |
|
|
46
|
+
| `loom generate resource <Name> --recipe module` | Backend-only module |
|
|
47
|
+
| `loom generate resource <Name> --recipe page` | Frontend page wired to an existing resource |
|
|
48
|
+
| `loom generate theme` / `loom generate deploy` | Import a shadcn theme / emit deploy configs |
|
|
49
|
+
| `loom check` | Verify project health — blueprint validity, anchor integrity, env file |
|
|
50
|
+
| `loom env [--sync]` | Diff `.env` against `.env.example`; `--sync` appends missing keys |
|
|
51
|
+
| `loom rename <name>` | Rebrand the CLI itself (bin name, help text, output) |
|
|
52
|
+
| `loom cleanup [preset]` | De-brand a project — `minimal` \| `production` (full) \| `template` |
|
|
53
|
+
| `loom customize` | Theme / layout / brand / data-display |
|
|
54
|
+
| `loom wizard` | Interactive guided setup |
|
|
55
|
+
| `loom doctor` | Environment + project health check |
|
|
56
|
+
| `loom rollback` | Undo the last generation |
|
|
57
|
+
| `loom finalize` | Lint + test + build for production |
|
|
58
|
+
| `loom preset [name]` | Apply a predefined preset |
|
|
59
|
+
| `loom remove <type> <name>` | Remove a generated resource and its references |
|
|
60
|
+
|
|
61
|
+
> `generate module`, `generate page`, and `make:resource` still work but are
|
|
62
|
+
> **superseded** by `generate resource` — they print a deprecation notice.
|
|
63
|
+
|
|
64
|
+
`loom init` is kept as an alias for `loom new`.
|
|
65
|
+
|
|
66
|
+
## Global flags
|
|
67
|
+
|
|
68
|
+
Every command honors:
|
|
69
|
+
|
|
70
|
+
- `--quiet` / `-q` — errors and warnings only (auto-on under CI / when piped)
|
|
71
|
+
- `--json` — structured JSON output for scripts
|
|
72
|
+
- `--no-color` — disable ANSI colour
|
|
73
|
+
- `--debug` — diagnostic detail
|
|
74
|
+
- `--yes` / `-y` — assume defaults, never prompt
|
|
75
|
+
|
|
76
|
+
## Generating a resource
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Full-stack CRUD: model, service, controller, routes, validator,
|
|
80
|
+
# admin pages, table/form components, API client, hooks — all mounted and linked.
|
|
81
|
+
loom generate resource Product --fields "name:string:required;price:number;slug:string"
|
|
82
|
+
|
|
83
|
+
# Choose how the create/edit form is mounted
|
|
84
|
+
loom generate resource Order --fields "total:number" --form-mode modal
|
|
85
|
+
|
|
86
|
+
# Pick the architecture level (see below)
|
|
87
|
+
loom generate resource Invoice --fields "amount:number" --arch lightweight
|
|
88
|
+
|
|
89
|
+
# Preview without writing
|
|
90
|
+
loom generate resource Ticket --fields "subject:string" --dry-run
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The engine creates the requested files **and links them**: mounts the route in
|
|
94
|
+
`backend/src/routes/index.js`, adds the lazy import + route to
|
|
95
|
+
`AppRouter.jsx`, and appends the nav entry to `app-preset.js`. Injection is
|
|
96
|
+
idempotent — re-running is safe.
|
|
97
|
+
|
|
98
|
+
### Architecture levels (`--arch`)
|
|
99
|
+
|
|
100
|
+
| Level | Backend shape |
|
|
101
|
+
|-------|---------------|
|
|
102
|
+
| `lightweight` | Inline controller, no service layer — minimal files |
|
|
103
|
+
| `moderate` *(default)* | Full layering — `models/`, `services/`, `controllers/`, `routes/` |
|
|
104
|
+
| `advanced` | `moderate` + generated tests + batch/transaction operations |
|
|
105
|
+
|
|
106
|
+
### Form modes (`--form-mode`)
|
|
107
|
+
|
|
108
|
+
`page` *(default)* · `modal` · `sidepanel` · `inline` — selects the list-page
|
|
109
|
+
shell and how the shared form component is mounted. One form component, four
|
|
110
|
+
thin page shells.
|
|
111
|
+
|
|
112
|
+
## Field spec
|
|
113
|
+
|
|
114
|
+
`--fields "name:type:rule|rule;name2:type2"` — e.g.
|
|
115
|
+
`"email:email:required|unique;age:number:min=0;bio:text"`. Types: `string`,
|
|
116
|
+
`text`, `number`, `boolean`, `date`, `email`, `password`, `ref`, `select`,
|
|
117
|
+
`image`, and more. Definitions can also come from a file: `--file resource.js`.
|
|
118
|
+
|
|
119
|
+
Inputs are schema-validated before generation runs — a bad field type, a
|
|
120
|
+
non-PascalCase name, or duplicate fields fail fast with a clear message.
|
|
121
|
+
|
|
122
|
+
## Customising templates
|
|
123
|
+
|
|
124
|
+
Templates resolve in three tiers (first match wins):
|
|
125
|
+
|
|
126
|
+
1. `<project>/.loom/templates/<path>` — project overrides
|
|
127
|
+
2. `~/.loom/templates/<path>` — user-global overrides
|
|
128
|
+
3. shipped defaults
|
|
129
|
+
|
|
130
|
+
A project can also override a recipe by pointing `blueprint.recipes.<name>` at
|
|
131
|
+
its own manifest.
|
|
132
|
+
|
|
133
|
+
## Rebranding the CLI
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
loom rename acme --display-name "ACME"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Updates `branding.json` and `package.json`'s `bin` key. Re-link with
|
|
140
|
+
`pnpm install` and the tool answers to `acme`.
|
|
141
|
+
|
|
142
|
+
## Preparing a project for handoff
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
loom cleanup production # full de-brand — see below
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`cleanup` refuses to run unless the working directory is a real project root
|
|
149
|
+
(has both `backend/` and `frontend/`). The `production` preset removes `.loom/`,
|
|
150
|
+
the bundled CLI, starter docs and demo content; strips `STARTER-KIT:` /
|
|
151
|
+
`TODO: Customize` comments and AUTO-GENERATED markers; resets package names;
|
|
152
|
+
and rewrites the README — leaving no trace of the starter kit.
|
|
153
|
+
|
|
154
|
+
## Local development
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
cd packages/cli
|
|
158
|
+
pnpm install
|
|
159
|
+
node bin/cli.js --help
|
|
160
|
+
pnpm test # vitest — engine, blueprint, recipes, services, schemas
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
See [`DEVELOPER.md`](./DEVELOPER.md) for engine internals, [`API.md`](./API.md)
|
|
164
|
+
for the programmatic API, and [`SPLIT.md`](./SPLIT.md) for the planned
|
|
165
|
+
`stackloom` / `stackloom-templates` repo split.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs-extra";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
// Read version from package.json
|
|
13
|
+
const pkg = JSON.parse(
|
|
14
|
+
readFileSync(new URL("../package.json", import.meta.url)),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// Branding — the CLI's own identity (rebrandable via branding.json / `loom rename`)
|
|
18
|
+
import { branding } from "../src/branding/index.js";
|
|
19
|
+
|
|
20
|
+
// Import commands
|
|
21
|
+
import init from "../src/commands/init.js";
|
|
22
|
+
import generateModule from "../src/commands/generate/module.js";
|
|
23
|
+
import generatePage from "../src/commands/generate/page.js";
|
|
24
|
+
import generateTheme from "../src/commands/generate/theme.js";
|
|
25
|
+
import generateDeploy from "../src/commands/generate/deploy.js";
|
|
26
|
+
import remove from "../src/commands/remove.js";
|
|
27
|
+
import wizard from "../src/commands/wizard.js";
|
|
28
|
+
import customize from "../src/commands/customize.js";
|
|
29
|
+
import cleanup from "../src/commands/cleanup.js";
|
|
30
|
+
import finalize from "../src/commands/finalize.js";
|
|
31
|
+
import rollback from "../src/commands/rollback.js";
|
|
32
|
+
import doctor from "../src/commands/doctor.js";
|
|
33
|
+
import preset from "../src/commands/preset.js";
|
|
34
|
+
import makeResource from "../src/commands/make/resource.js";
|
|
35
|
+
import generateResource from "../src/commands/generate-resource.js";
|
|
36
|
+
import check from "../src/commands/check.js";
|
|
37
|
+
import env from "../src/commands/env.js";
|
|
38
|
+
import rename from "../src/commands/rename.js";
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.name(branding.binName)
|
|
42
|
+
.description(branding.description)
|
|
43
|
+
.version(pkg.version)
|
|
44
|
+
// Global output flags — consumed via reporterFromOptions(program.opts()).
|
|
45
|
+
.option("-q, --quiet", "Errors and warnings only (auto-on under CI / when piped)")
|
|
46
|
+
.option("--json", "Structured JSON output for scripts and CI")
|
|
47
|
+
.option("--no-color", "Disable ANSI colour")
|
|
48
|
+
.option("--debug", "Show diagnostic detail")
|
|
49
|
+
.option("-y, --yes", "Assume defaults; never prompt (fails fast on missing input)");
|
|
50
|
+
|
|
51
|
+
// Init: create fresh project from template (always new copy)
|
|
52
|
+
program
|
|
53
|
+
.command("init [project-name]")
|
|
54
|
+
.description("Create a new project from the starter kit template")
|
|
55
|
+
.option(
|
|
56
|
+
"--preset <variant>",
|
|
57
|
+
"Preset: saas|clinic|studio|operations|commerce|custom",
|
|
58
|
+
)
|
|
59
|
+
.option(
|
|
60
|
+
"--theme <theme>",
|
|
61
|
+
"Design theme: executiveBlue|clinicSoft|studioElevated|operationsDense|commerceWarm",
|
|
62
|
+
)
|
|
63
|
+
.option(
|
|
64
|
+
"--layout <layout>",
|
|
65
|
+
"Layout: hybridSaas|sidebarWorkspace|topbarPortal|rightRailStudio",
|
|
66
|
+
)
|
|
67
|
+
.option("--brand-name <name>", "Brand name")
|
|
68
|
+
.option("--tagline <text>", "Brand tagline")
|
|
69
|
+
.option(
|
|
70
|
+
"--extra-modules <list>",
|
|
71
|
+
"Comma-separated backend modules to include (e.g., users,products)",
|
|
72
|
+
)
|
|
73
|
+
.option(
|
|
74
|
+
"--deploy-targets <list>",
|
|
75
|
+
"Comma-separated deploy targets (docker,vercel,railway)",
|
|
76
|
+
)
|
|
77
|
+
.option("--no-install", "Skip pnpm install")
|
|
78
|
+
.option("--target <dir>", "Output directory")
|
|
79
|
+
.option("--force", "Overwrite existing directory")
|
|
80
|
+
.action(init);
|
|
81
|
+
|
|
82
|
+
// Generate commands (inside existing project)
|
|
83
|
+
const generateCmd = program
|
|
84
|
+
.command("generate")
|
|
85
|
+
.description("Add features to existing project");
|
|
86
|
+
|
|
87
|
+
// Unified, engine-backed generation — blueprint + recipe + transactional pipeline.
|
|
88
|
+
generateCmd
|
|
89
|
+
.command("resource [name]")
|
|
90
|
+
.description(
|
|
91
|
+
"Generate a full-stack resource via the engine (recipe-driven, transactional, validated)",
|
|
92
|
+
)
|
|
93
|
+
.option("--fields <spec>", "Field spec: 'name:type:rules;...'")
|
|
94
|
+
.option("--file <path>", "Path to a resource definition file")
|
|
95
|
+
.option("--recipe <name>", "Recipe to run: resource|module|page", "resource")
|
|
96
|
+
.option("--arch <level>", "Architecture: lightweight|moderate|advanced", "moderate")
|
|
97
|
+
.option("--form-mode <mode>", "Form mount mode: page|modal|sidepanel|inline", "page")
|
|
98
|
+
.option("--with-tests", "Generate test files")
|
|
99
|
+
.option("--no-frontend", "Skip frontend generation")
|
|
100
|
+
.option("--dry-run", "Preview the file plan without writing")
|
|
101
|
+
.action((name, options) =>
|
|
102
|
+
generateResource(options.recipe || "resource", name, { ...program.opts(), ...options }),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
generateCmd
|
|
106
|
+
.command("module <name>")
|
|
107
|
+
.description(
|
|
108
|
+
"Generate backend module (model, service, controller, routes, validator)",
|
|
109
|
+
)
|
|
110
|
+
.option("--force", "Overwrite existing files")
|
|
111
|
+
.option("--fields <spec>", "Field specification")
|
|
112
|
+
.option("--interactive", "Prompt for fields interactively")
|
|
113
|
+
.option(
|
|
114
|
+
"--architecture <level>",
|
|
115
|
+
"Architecture: lightweight|moderate|advanced",
|
|
116
|
+
"moderate",
|
|
117
|
+
)
|
|
118
|
+
.option("--with-page", "Generate corresponding frontend page")
|
|
119
|
+
.option(
|
|
120
|
+
"--form-mode <mode>",
|
|
121
|
+
"Form display mode if --with-page: page|modal|sidepanel|inline",
|
|
122
|
+
"page",
|
|
123
|
+
)
|
|
124
|
+
.action(generateModule);
|
|
125
|
+
|
|
126
|
+
generateCmd
|
|
127
|
+
.command("page <name>")
|
|
128
|
+
.description("Generate frontend page with route and nav entry")
|
|
129
|
+
.option("--route <path>", "Custom route path")
|
|
130
|
+
.option("--no-nav", "Do not add to navigation")
|
|
131
|
+
.option("--icon <name>", "Icon name from lucide-react")
|
|
132
|
+
.option("--force", "Overwrite existing files")
|
|
133
|
+
.option("--with-form", "Generate form component")
|
|
134
|
+
.option(
|
|
135
|
+
"--form-mode <mode>",
|
|
136
|
+
"Form display mode: page|modal|sidepanel|inline",
|
|
137
|
+
"page",
|
|
138
|
+
)
|
|
139
|
+
.option("--form-fields <spec>", "Form field specification")
|
|
140
|
+
.option("--interactive", "Prompt for form fields interactively")
|
|
141
|
+
.action(generatePage);
|
|
142
|
+
|
|
143
|
+
generateCmd
|
|
144
|
+
.command("theme")
|
|
145
|
+
.description("Import a shadcn/ui theme from CSS variables")
|
|
146
|
+
.option("--file <path>", "Path to CSS file with :root/.dark")
|
|
147
|
+
.option("--paste <css>", "CSS string directly")
|
|
148
|
+
.option("--fallback <theme>", "Fallback theme (default: executiveBlue)")
|
|
149
|
+
.option("--appearance <recipe>", "Appearance: elevated|flat|ux-heavy")
|
|
150
|
+
.option("--apply", "Update app-preset.js automatically")
|
|
151
|
+
.action(generateTheme);
|
|
152
|
+
|
|
153
|
+
generateCmd
|
|
154
|
+
.command("deploy")
|
|
155
|
+
.description("Generate deployment configs (Docker, Vercel, Railway)")
|
|
156
|
+
.option("--target <provider>", "Target: docker|vercel|railway|all")
|
|
157
|
+
.option("--force", "Overwrite existing files")
|
|
158
|
+
.action(generateDeploy);
|
|
159
|
+
|
|
160
|
+
// Remove generated resources (safe, with confirmation)
|
|
161
|
+
program
|
|
162
|
+
.command("remove <type> <name>")
|
|
163
|
+
.description("Remove a generated page or module (with cleanup)")
|
|
164
|
+
.option("--force", "Skip confirmation")
|
|
165
|
+
.action(remove);
|
|
166
|
+
|
|
167
|
+
// Cleanup: remove demo files, strip branding, prepare for deployment
|
|
168
|
+
program
|
|
169
|
+
.command("cleanup [preset]")
|
|
170
|
+
.description("Clean up / de-brand the project (minimal|production|template)")
|
|
171
|
+
.action((preset) => cleanup(preset));
|
|
172
|
+
|
|
173
|
+
// Interactive wizard — guided setup after init or anytime
|
|
174
|
+
program
|
|
175
|
+
.command("wizard")
|
|
176
|
+
.description("Interactive guide to extend your project")
|
|
177
|
+
.option("--skip-confirm", "Skip final confirmation step")
|
|
178
|
+
.action(wizard);
|
|
179
|
+
|
|
180
|
+
// Customize existing project: theme/layout/brand/data
|
|
181
|
+
const customizeCmd = program
|
|
182
|
+
.command("customize")
|
|
183
|
+
.description("Customize project design & branding");
|
|
184
|
+
|
|
185
|
+
// ── Theme subcommand group ──
|
|
186
|
+
const themeCmd = customizeCmd.command("theme").description("Theme operations");
|
|
187
|
+
themeCmd
|
|
188
|
+
.command("set [theme]")
|
|
189
|
+
.description("Switch to a built-in theme")
|
|
190
|
+
.action(customize.customizeThemeSet);
|
|
191
|
+
themeCmd
|
|
192
|
+
.command("import")
|
|
193
|
+
.description("Import a custom shadcn/ui theme from CSS")
|
|
194
|
+
.option("--file <path>", "Path to CSS file with :root and .dark")
|
|
195
|
+
.option("--paste <css>", "CSS string directly")
|
|
196
|
+
.option("--fallback <theme>", "Fallback theme (default: executiveBlue)")
|
|
197
|
+
.option("--appearance <recipe>", "Appearance recipe (default: quiet)")
|
|
198
|
+
.action(customize.customizeThemeImport);
|
|
199
|
+
|
|
200
|
+
// ── Layout ──
|
|
201
|
+
const layoutCmd = customizeCmd
|
|
202
|
+
.command("layout")
|
|
203
|
+
.description("Layout operations");
|
|
204
|
+
layoutCmd
|
|
205
|
+
.command("set [layout]")
|
|
206
|
+
.description("Switch layout shell")
|
|
207
|
+
.action(customize.customizeLayoutSet);
|
|
208
|
+
|
|
209
|
+
// ── Brand ──
|
|
210
|
+
const brandCmd = customizeCmd.command("brand").description("Brand operations");
|
|
211
|
+
brandCmd
|
|
212
|
+
.command("set")
|
|
213
|
+
.description("Update brand name and/or tagline")
|
|
214
|
+
.option("--name <text>", "New brand name")
|
|
215
|
+
.option("--tagline <text>", "New tagline")
|
|
216
|
+
.action(customize.customizeBrandSet);
|
|
217
|
+
|
|
218
|
+
// ── Data display ──
|
|
219
|
+
const dataCmd = customizeCmd
|
|
220
|
+
.command("data")
|
|
221
|
+
.description("Data display template operations");
|
|
222
|
+
dataCmd
|
|
223
|
+
.command("set [template]")
|
|
224
|
+
.description("Switch data display template")
|
|
225
|
+
.action(customize.customizeDataSet);
|
|
226
|
+
|
|
227
|
+
// ── Discovery helpers ──
|
|
228
|
+
customizeCmd
|
|
229
|
+
.command("list-themes")
|
|
230
|
+
.description("List available built-in themes")
|
|
231
|
+
.action(customize.customizeListThemes);
|
|
232
|
+
customizeCmd
|
|
233
|
+
.command("list-layouts")
|
|
234
|
+
.description("List available layout shells")
|
|
235
|
+
.action(customize.customizeListLayouts);
|
|
236
|
+
customizeCmd
|
|
237
|
+
.command("list-data")
|
|
238
|
+
.description("List available data display templates")
|
|
239
|
+
.action(customize.customizeListData);
|
|
240
|
+
|
|
241
|
+
// Finalize
|
|
242
|
+
program
|
|
243
|
+
.command("finalize")
|
|
244
|
+
.description("Prepare project for production (lint, test, build)")
|
|
245
|
+
.action(finalize);
|
|
246
|
+
|
|
247
|
+
// Rollback
|
|
248
|
+
program
|
|
249
|
+
.command("rollback")
|
|
250
|
+
.description("Undo the last generation action")
|
|
251
|
+
.option("-f, --force", "Skip confirmation")
|
|
252
|
+
.option("-v, --verbose", "Show detailed logs")
|
|
253
|
+
.action(rollback);
|
|
254
|
+
|
|
255
|
+
// Doctor
|
|
256
|
+
program
|
|
257
|
+
.command("doctor")
|
|
258
|
+
.description("Check environment and project health")
|
|
259
|
+
.action(doctor);
|
|
260
|
+
|
|
261
|
+
// Check — structural health: Node, blueprint validity, anchor integrity, env file
|
|
262
|
+
program
|
|
263
|
+
.command("check")
|
|
264
|
+
.description("Verify project + environment health (blueprint, anchors, env)")
|
|
265
|
+
.action((options) => check({ ...program.opts(), ...options }));
|
|
266
|
+
|
|
267
|
+
// Env — keep .env in sync with .env.example
|
|
268
|
+
program
|
|
269
|
+
.command("env")
|
|
270
|
+
.description("Diff .env against .env.example; --sync appends missing keys")
|
|
271
|
+
.option("--sync", "Append missing keys to .env")
|
|
272
|
+
.action((options) => env({ ...program.opts(), ...options }));
|
|
273
|
+
|
|
274
|
+
// Preset
|
|
275
|
+
program
|
|
276
|
+
.command("preset [name]")
|
|
277
|
+
.description("Apply a predefined configuration preset (saas, clinic, etc.)")
|
|
278
|
+
.action(preset);
|
|
279
|
+
|
|
280
|
+
// Make Resource
|
|
281
|
+
program
|
|
282
|
+
.command("make:resource [name]")
|
|
283
|
+
.description("Create a new resource from schema or interactive wizard")
|
|
284
|
+
.option("-f, --file <path>", "Path to resource definition file")
|
|
285
|
+
.option("--fields <spec>", "Field specification (name:type:rules;...)")
|
|
286
|
+
.option("-i, --interactive", "Run interactive wizard")
|
|
287
|
+
.option("--dry-run", "Preview changes without writing")
|
|
288
|
+
.option("--force", "Overwrite existing files")
|
|
289
|
+
.option(
|
|
290
|
+
"--arch <level>",
|
|
291
|
+
"Architecture: lightweight|moderate|advanced",
|
|
292
|
+
"moderate",
|
|
293
|
+
)
|
|
294
|
+
.option("--no-frontend", "Skip frontend generation")
|
|
295
|
+
.option("--with-tests", "Generate test files")
|
|
296
|
+
.action(makeResource);
|
|
297
|
+
|
|
298
|
+
// Rename: rebrand the CLI tool itself
|
|
299
|
+
program
|
|
300
|
+
.command("rename <new-name>")
|
|
301
|
+
.description("Rebrand this CLI — change the command name used to invoke it")
|
|
302
|
+
.option("--display-name <name>", "Human-readable display name")
|
|
303
|
+
.option("--description <text>", "CLI description shown in help")
|
|
304
|
+
.action(rename);
|
|
305
|
+
|
|
306
|
+
program.parse();
|
package/branding.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stackloom-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Stackloom CLI — weave production-ready full-stack apps. Recipe-driven, transactional code generation for MERN and beyond.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"loom": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "node --watch bin/cli.js",
|
|
12
|
+
"start": "node bin/cli.js",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"test:coverage": "vitest run --coverage",
|
|
16
|
+
"prepublishOnly": "vitest run"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"stackloom",
|
|
20
|
+
"loom",
|
|
21
|
+
"mern",
|
|
22
|
+
"scaffold",
|
|
23
|
+
"generator",
|
|
24
|
+
"cli",
|
|
25
|
+
"react",
|
|
26
|
+
"express",
|
|
27
|
+
"mongodb",
|
|
28
|
+
"starter",
|
|
29
|
+
"fullstack"
|
|
30
|
+
],
|
|
31
|
+
"author": "Niyo Abou Sharif <niyoabousharifu@gmail.com>",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/Abou-Sharif/stackloom.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/Abou-Sharif/stackloom#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/Abou-Sharif/stackloom/issues"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"chalk": "^5.3.0",
|
|
43
|
+
"commander": "^13.1.0",
|
|
44
|
+
"ejs": "^5.0.2",
|
|
45
|
+
"fs-extra": "^11.3.0",
|
|
46
|
+
"inquirer": "^13.3.2",
|
|
47
|
+
"ora": "^8.0.1",
|
|
48
|
+
"tar": "^7.5.1"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18.0.0"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"bin",
|
|
55
|
+
"src",
|
|
56
|
+
"branding.json",
|
|
57
|
+
"README.md",
|
|
58
|
+
"LICENSE"
|
|
59
|
+
],
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/fs-extra": "^11.0.4",
|
|
65
|
+
"@types/inquirer": "^9.0.9",
|
|
66
|
+
"@types/node": "^25.7.0",
|
|
67
|
+
"@types/tar": "^7.0.87",
|
|
68
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
69
|
+
"typescript": "^6.0.3",
|
|
70
|
+
"vitest": "^4.1.6"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
import os from "os";
|
|
6
|
+
|
|
7
|
+
const CLI_PATH = path.resolve(__dirname, "../../bin/cli.js");
|
|
8
|
+
const TEMP_DIR = path.join(os.tmpdir(), "loom-test-" + Date.now());
|
|
9
|
+
|
|
10
|
+
describe("Stackloom CLI Smoke Tests", () => {
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
await fs.ensureDir(TEMP_DIR);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await fs.remove(TEMP_DIR);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Spawning a fresh Node process per test is slow on Windows + pnpm symlinks;
|
|
20
|
+
// give the smoke tests generous timeouts — they assert behaviour, not speed.
|
|
21
|
+
const SMOKE_TIMEOUT = 30000;
|
|
22
|
+
|
|
23
|
+
it("should show version", () => {
|
|
24
|
+
const output = execSync(`node "${CLI_PATH}" --version`).toString();
|
|
25
|
+
expect(output).toMatch(/\d+\.\d+\.\d+/);
|
|
26
|
+
}, SMOKE_TIMEOUT);
|
|
27
|
+
|
|
28
|
+
it("should show help", () => {
|
|
29
|
+
const output = execSync(`node "${CLI_PATH}" --help`).toString();
|
|
30
|
+
expect(output).toContain("Usage: loom");
|
|
31
|
+
expect(output).toContain("init");
|
|
32
|
+
expect(output).toContain("generate");
|
|
33
|
+
}, SMOKE_TIMEOUT);
|
|
34
|
+
|
|
35
|
+
it("should fail when running doctor outside project", () => {
|
|
36
|
+
let failed = false;
|
|
37
|
+
try {
|
|
38
|
+
execSync(`node "${CLI_PATH}" doctor`, { cwd: TEMP_DIR, stdio: "pipe" });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
failed = true;
|
|
41
|
+
const out = `${err.stdout?.toString() ?? ""}${err.stderr?.toString() ?? ""}`;
|
|
42
|
+
expect(out).toMatch(/not a|MERN|project/i);
|
|
43
|
+
}
|
|
44
|
+
expect(failed).toBe(true);
|
|
45
|
+
}, SMOKE_TIMEOUT);
|
|
46
|
+
});
|