@roboticela/devkit 3.0.0 → 4.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/README.md CHANGED
@@ -180,6 +180,12 @@ devkit init
180
180
  Install one or more components into the current project.
181
181
 
182
182
  ```bash
183
+ # Interactive — browse the registry for your template, multi-select, then pick variants when needed
184
+ devkit add
185
+
186
+ # Same as above but only print what would be installed
187
+ devkit add --dry-run
188
+
183
189
  # Single component
184
190
  devkit add auth
185
191
 
@@ -196,6 +202,8 @@ devkit add auth@2.0.0
196
202
  devkit add auth --dry-run
197
203
  ```
198
204
 
205
+ **Interactive mode** (no component names): DevKit loads components from the registry filtered by your template in `devkit.config.json`, excludes anything already in `devkit.lock.json`, and opens a terminal multiselect (space to toggle, enter to confirm). For each selected package that has variants, you get a follow-up prompt unless you passed `--variant=<id>` and that id exists for that component.
206
+
199
207
  **What happens under the hood:**
200
208
 
201
209
  1. Resolves the component version (latest unless pinned)
@@ -365,6 +373,10 @@ devkit install
365
373
  Stop DevKit from tracking a component without deleting its files. After ejecting, the files become entirely yours — no more updates via `devkit update`.
366
374
 
367
375
  ```bash
376
+ # Interactive — multiselect from installed components (from devkit.lock.json)
377
+ devkit eject
378
+
379
+ # Eject one component by name
368
380
  devkit eject auth
369
381
  ```
370
382
 
@@ -1,27 +1,126 @@
1
1
  import ora from "ora";
2
2
  import chalk from "chalk";
3
+ import { intro, outro, multiselect, select, isCancel } from "@clack/prompts";
3
4
  import { readConfig, isComponentInstalled } from "../lib/config.js";
4
5
  import { installComponent } from "../lib/installer.js";
6
+ import { listComponents } from "../lib/registry.js";
5
7
  import { log } from "../lib/logger.js";
8
+ /** `undefined` = no variant; string = chosen id; `"cancel"` = user aborted */
9
+ async function pickVariantForComponent(meta, optsVariant) {
10
+ if (!meta.hasVariants || !meta.variants?.length)
11
+ return undefined;
12
+ if (meta.variants.length === 1)
13
+ return meta.variants[0].id;
14
+ if (optsVariant) {
15
+ const match = meta.variants.find((v) => v.id === optsVariant);
16
+ if (match)
17
+ return match.id;
18
+ }
19
+ const v = await select({
20
+ message: `Variant for ${meta.name}`,
21
+ options: meta.variants.map((x) => ({
22
+ value: x.id,
23
+ label: x.label,
24
+ hint: x.description,
25
+ })),
26
+ });
27
+ if (isCancel(v))
28
+ return "cancel";
29
+ return v;
30
+ }
31
+ async function interactiveAddSelections(config, opts, cwd) {
32
+ let all;
33
+ try {
34
+ all = await listComponents(config.template);
35
+ }
36
+ catch (e) {
37
+ log.error(`Could not reach registry: ${e.message}`);
38
+ log.info("Check DEVKIT_REGISTRY or start the registry locally (see repo README).");
39
+ return null;
40
+ }
41
+ const installable = all.filter((c) => !isComponentInstalled(c.name, cwd));
42
+ if (installable.length === 0) {
43
+ log.info("Every component from the registry for your template is already installed.");
44
+ return [];
45
+ }
46
+ console.log();
47
+ intro(chalk.cyan("Add components"));
48
+ const pickedRaw = await multiselect({
49
+ message: "Select components to install (space to toggle, enter to confirm)",
50
+ options: installable.map((c) => ({
51
+ value: c.name,
52
+ label: c.name,
53
+ hint: c.description.length > 64 ? `${c.description.slice(0, 61)}…` : c.description,
54
+ })),
55
+ required: false,
56
+ });
57
+ if (isCancel(pickedRaw)) {
58
+ outro("Cancelled.");
59
+ return [];
60
+ }
61
+ const picked = pickedRaw;
62
+ if (picked.length === 0) {
63
+ outro("No components selected.");
64
+ return [];
65
+ }
66
+ const results = [];
67
+ for (const compName of picked) {
68
+ const meta = installable.find((x) => x.name === compName);
69
+ const variant = await pickVariantForComponent(meta, opts.variant);
70
+ if (variant === "cancel") {
71
+ outro("Cancelled.");
72
+ return [];
73
+ }
74
+ results.push({ name: compName, version: "latest", variant });
75
+ }
76
+ return results;
77
+ }
6
78
  export async function addCommand(names, opts, cwd = process.cwd()) {
7
79
  const config = readConfig(cwd);
8
- for (const nameAtVersion of names) {
9
- const [name, version = "latest"] = nameAtVersion.split("@");
80
+ const usedInteractive = names.length === 0;
81
+ let todo;
82
+ if (usedInteractive) {
83
+ const picked = await interactiveAddSelections(config, opts, cwd);
84
+ if (picked === null)
85
+ return;
86
+ if (picked.length === 0)
87
+ return;
88
+ todo = picked;
89
+ }
90
+ else {
91
+ todo = names.map((nameAtVersion) => {
92
+ const [name, version = "latest"] = nameAtVersion.split("@");
93
+ return { name, version, variant: opts.variant };
94
+ });
95
+ }
96
+ let anyOk = false;
97
+ for (const { name, version, variant } of todo) {
10
98
  if (isComponentInstalled(name, cwd)) {
11
99
  log.warn(`${name} is already installed. Run 'devkit update ${name}' to update.`);
12
100
  continue;
13
101
  }
14
102
  if (opts.dryRun) {
15
- log.info(`[dry-run] Would install: ${chalk.cyan(name)}@${version} (${config.template})`);
103
+ log.info(`[dry-run] Would install: ${chalk.cyan(name)}@${version} (${config.template})` +
104
+ (variant ? chalk.dim(` variant=${variant}`) : ""));
105
+ anyOk = true;
16
106
  continue;
17
107
  }
18
108
  const spinner = ora({ text: `Installing ${chalk.cyan(name)}…`, color: "cyan" }).start();
19
109
  try {
20
- await installComponent(name, config.template, version, opts.variant, cwd);
110
+ await installComponent(name, config.template, version, variant, cwd);
21
111
  spinner.succeed(chalk.green(`${name}@${version} installed`));
112
+ anyOk = true;
22
113
  }
23
114
  catch (e) {
24
115
  spinner.fail(chalk.red(`Failed to install ${name}: ${e.message}`));
25
116
  }
26
117
  }
118
+ if (usedInteractive) {
119
+ if (anyOk) {
120
+ outro(opts.dryRun ? chalk.dim("Dry run complete.") : chalk.green("Done."));
121
+ }
122
+ else if (!opts.dryRun) {
123
+ outro(chalk.yellow("Nothing was installed."));
124
+ }
125
+ }
27
126
  }
@@ -0,0 +1 @@
1
+ export declare function ejectCommand(name: string | undefined, cwd?: string): Promise<void>;
@@ -0,0 +1,60 @@
1
+ import { intro, outro, multiselect, isCancel } from "@clack/prompts";
2
+ import chalk from "chalk";
3
+ import { unlinkSync, existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { readLock, writeLock } from "../lib/config.js";
6
+ import { log } from "../lib/logger.js";
7
+ export async function ejectCommand(name, cwd = process.cwd()) {
8
+ const lock = readLock(cwd);
9
+ const installed = Object.keys(lock.components);
10
+ if (installed.length === 0) {
11
+ log.info("No installed components to eject.");
12
+ return;
13
+ }
14
+ let toEject;
15
+ if (name) {
16
+ if (!(name in lock.components)) {
17
+ log.warn(`'${name}' is not an installed DevKit component.`);
18
+ return;
19
+ }
20
+ toEject = [name];
21
+ }
22
+ else {
23
+ console.log();
24
+ intro(chalk.cyan("Eject components"));
25
+ const picked = await multiselect({
26
+ message: "Select components to eject (DevKit stops tracking them; files stay on disk — space to toggle, enter to confirm)",
27
+ options: installed.map((n) => ({
28
+ value: n,
29
+ label: n,
30
+ hint: `v${lock.components[n].version}`,
31
+ })),
32
+ required: false,
33
+ });
34
+ if (isCancel(picked)) {
35
+ outro("Cancelled.");
36
+ return;
37
+ }
38
+ const list = picked;
39
+ if (list.length === 0) {
40
+ outro("No components selected.");
41
+ return;
42
+ }
43
+ toEject = list;
44
+ }
45
+ for (const n of toEject) {
46
+ if (!(n in lock.components)) {
47
+ log.warn(`Skipping '${n}' — not in lock file.`);
48
+ continue;
49
+ }
50
+ const manifestPath = join(cwd, ".devkit", `${n}.manifest.json`);
51
+ if (existsSync(manifestPath))
52
+ unlinkSync(manifestPath);
53
+ delete lock.components[n];
54
+ log.success(`${n} ejected. Files are yours — DevKit will no longer manage them.`);
55
+ }
56
+ writeLock(lock, cwd);
57
+ if (!name) {
58
+ outro(chalk.green("Done."));
59
+ }
60
+ }
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { listCommand } from "./commands/list.js";
9
9
  import { infoCommand } from "./commands/info.js";
10
10
  import { updateCommand, upgradeAllCommand } from "./commands/update.js";
11
11
  import { doctorCommand } from "./commands/doctor.js";
12
+ import { ejectCommand } from "./commands/eject.js";
12
13
  import { themeApplyCommand, themePreviewCommand, themePresetCommand, themeSetCommand, themeListCommand, themeAuditCommand, } from "./commands/theme.js";
13
14
  const program = new Command();
14
15
  program
@@ -36,11 +37,11 @@ program
36
37
  .action(() => initCommand());
37
38
  // ── devkit add ────────────────────────────────────────────────────────────────
38
39
  program
39
- .command("add <components...>")
40
- .description("Install one or more components (e.g. devkit add auth hero-section)")
41
- .option("--variant <id>", "Variant to install for components that have variants")
40
+ .command("add [components...]")
41
+ .description("Install components run with no args for an interactive picker")
42
+ .option("--variant <id>", "Variant for components with variants (non-interactive or as default when prompting)")
42
43
  .option("--dry-run", "Preview what would be installed without writing files")
43
- .action((names, opts) => addCommand(names, opts));
44
+ .action((names, opts) => addCommand(names ?? [], opts));
44
45
  // ── devkit remove ─────────────────────────────────────────────────────────────
45
46
  program
46
47
  .command("remove <name>")
@@ -124,28 +125,18 @@ program
124
125
  });
125
126
  // ── devkit eject ──────────────────────────────────────────────────────────────
126
127
  program
127
- .command("eject <name>")
128
- .description("Stop DevKit from tracking a component (files are kept, no more updates via DevKit)")
129
- .action(async (name) => {
130
- const { readLock, writeLock } = await import("./lib/config.js");
131
- const { unlinkSync, existsSync } = await import("fs");
132
- const { join } = await import("path");
133
- const { log } = await import("./lib/logger.js");
134
- const manifestPath = join(process.cwd(), ".devkit", `${name}.manifest.json`);
135
- if (existsSync(manifestPath))
136
- unlinkSync(manifestPath);
137
- const lock = readLock();
138
- delete lock.components[name];
139
- writeLock(lock);
140
- log.success(`${name} ejected. Files are yours — DevKit will no longer manage them.`);
141
- });
128
+ .command("eject [name]")
129
+ .description("Stop DevKit from tracking component(s) run with no args to pick interactively")
130
+ .action((name) => ejectCommand(name));
142
131
  // ── Help footer ───────────────────────────────────────────────────────────────
143
132
  program.addHelpText("after", `
144
133
  ${chalk.bold("Examples:")}
145
134
  ${chalk.cyan("devkit create my-app")} Interactive project creation
146
135
  ${chalk.cyan("devkit create my-app --template=nextjs-compact --yes")} Non-interactive
136
+ ${chalk.cyan("devkit add")} Interactive component picker
147
137
  ${chalk.cyan("devkit add auth")} Install auth component
148
138
  ${chalk.cyan("devkit add hero-section --variant=split-image")}
139
+ ${chalk.cyan("devkit eject")} Pick component(s) to eject
149
140
  ${chalk.cyan("devkit theme set colors.primary #e11d48")} Change primary color
150
141
  ${chalk.cyan("devkit doctor")} Check configuration
151
142
  ${chalk.cyan("devkit list")} Browse all components
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roboticela/devkit",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "Roboticela DevKit CLI — scaffold, extend, and theme full-stack projects with one command",
5
5
  "type": "module",
6
6
  "bin": {