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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/bin/cli.js +306 -0
  4. package/branding.json +8 -0
  5. package/package.json +72 -0
  6. package/src/__tests__/cli-smoke.test.js +46 -0
  7. package/src/blueprint/__tests__/blueprint.test.js +116 -0
  8. package/src/blueprint/blueprint.js +181 -0
  9. package/src/blueprint/default.blueprint.json +78 -0
  10. package/src/blueprint/index.js +10 -0
  11. package/src/blueprint/loader.js +101 -0
  12. package/src/blueprint/schema-kit.js +161 -0
  13. package/src/blueprint/schema.js +78 -0
  14. package/src/branding/__tests__/branding.test.js +49 -0
  15. package/src/branding/index.js +48 -0
  16. package/src/commands/__tests__/commands.test.js +83 -0
  17. package/src/commands/check.js +71 -0
  18. package/src/commands/cleanup.js +347 -0
  19. package/src/commands/customize.js +263 -0
  20. package/src/commands/doctor.js +84 -0
  21. package/src/commands/env.js +75 -0
  22. package/src/commands/finalize.js +68 -0
  23. package/src/commands/generate/ci-cd.js +378 -0
  24. package/src/commands/generate/deploy-advanced.js +253 -0
  25. package/src/commands/generate/deploy.js +99 -0
  26. package/src/commands/generate/env.template.js +221 -0
  27. package/src/commands/generate/index.js +7 -0
  28. package/src/commands/generate/module.js +836 -0
  29. package/src/commands/generate/page.js +1415 -0
  30. package/src/commands/generate/test-scaffold.js +279 -0
  31. package/src/commands/generate/theme.js +67 -0
  32. package/src/commands/generate-resource.js +133 -0
  33. package/src/commands/index.js +9 -0
  34. package/src/commands/init.js +350 -0
  35. package/src/commands/make/resource.js +298 -0
  36. package/src/commands/preset.js +57 -0
  37. package/src/commands/remove.js +170 -0
  38. package/src/commands/rename.js +54 -0
  39. package/src/commands/rollback.js +90 -0
  40. package/src/commands/wizard.js +303 -0
  41. package/src/core/__tests__/generator.test.js +67 -0
  42. package/src/core/__tests__/marker-strategy.test.js +57 -0
  43. package/src/core/__tests__/resource-definition.test.js +32 -0
  44. package/src/core/generator.js +542 -0
  45. package/src/core/marker-strategy.js +138 -0
  46. package/src/core/resource-definition.js +346 -0
  47. package/src/core/state-tracker.js +67 -0
  48. package/src/core/template-loader.js +163 -0
  49. package/src/engine/__tests__/engine.test.js +306 -0
  50. package/src/engine/index.js +21 -0
  51. package/src/engine/injector.js +198 -0
  52. package/src/engine/pipeline.js +138 -0
  53. package/src/engine/transaction.js +105 -0
  54. package/src/engine/validator.js +190 -0
  55. package/src/index.js +4 -0
  56. package/src/recipes/__tests__/recipe.test.js +128 -0
  57. package/src/recipes/builtin/module.json +22 -0
  58. package/src/recipes/builtin/page.json +21 -0
  59. package/src/recipes/builtin/resource.json +35 -0
  60. package/src/recipes/condition.js +147 -0
  61. package/src/recipes/index.js +11 -0
  62. package/src/recipes/loader.js +95 -0
  63. package/src/recipes/recipe.js +89 -0
  64. package/src/recipes/schema.js +47 -0
  65. package/src/schemas/__tests__/schemas.test.js +67 -0
  66. package/src/schemas/index.js +18 -0
  67. package/src/schemas/options.js +38 -0
  68. package/src/schemas/resource.js +112 -0
  69. package/src/services/__tests__/reporter.test.js +98 -0
  70. package/src/services/clock.js +31 -0
  71. package/src/services/index.js +43 -0
  72. package/src/services/reporter.js +136 -0
  73. package/src/templates/resource/api.js.ejs +39 -0
  74. package/src/templates/resource/components/form.jsx.ejs +81 -0
  75. package/src/templates/resource/components/table.jsx.ejs +68 -0
  76. package/src/templates/resource/controller.js.ejs +154 -0
  77. package/src/templates/resource/hooks.js.ejs +46 -0
  78. package/src/templates/resource/model.js.ejs +64 -0
  79. package/src/templates/resource/page-detail.jsx.ejs +55 -0
  80. package/src/templates/resource/page-form.jsx.ejs +30 -0
  81. package/src/templates/resource/page-inline.jsx.ejs +74 -0
  82. package/src/templates/resource/page-modal.jsx.ejs +98 -0
  83. package/src/templates/resource/page-page.jsx.ejs +99 -0
  84. package/src/templates/resource/page-sidepanel.jsx.ejs +100 -0
  85. package/src/templates/resource/routes.js.ejs +35 -0
  86. package/src/templates/resource/service.js.ejs +132 -0
  87. package/src/templates/resource/test.ejs +71 -0
  88. package/src/templates/resource/types.ts.ejs +17 -0
  89. package/src/templates/resource/validator.js.ejs +26 -0
  90. package/src/templates/snippets/lazy-import.ejs +1 -0
  91. package/src/templates/snippets/nav-entry.ejs +1 -0
  92. package/src/templates/snippets/route-entry.ejs +5 -0
  93. package/src/templates/snippets/route-mount.ejs +1 -0
  94. package/src/utils/fieldValidators.js +371 -0
  95. package/src/utils/logging/logger.js +47 -0
  96. package/src/utils/namingUtils.js +38 -0
  97. 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
@@ -0,0 +1,8 @@
1
+ {
2
+ "binName": "loom",
3
+ "displayName": "Stackloom",
4
+ "description": "Stackloom — weave production-ready full-stack apps from a single command",
5
+ "tagline": "Weave full-stack apps from a single command",
6
+ "stateDirName": ".loom",
7
+ "packageName": "stackloom"
8
+ }
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
+ });