@glasshome/widget-cli 0.1.0 → 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/README.md CHANGED
@@ -11,20 +11,28 @@ bun add -g @glasshome/widget-cli
11
11
  ## Quick Start
12
12
 
13
13
  ```bash
14
- bunx @glasshome/widget-cli create my-widget
15
- cd my-widget
16
- bun install
17
- bun run dev
14
+ # Create a new widget project (interactive)
15
+ bunx @glasshome/widget-cli
16
+
17
+ # Or explicitly
18
+ bunx @glasshome/widget-cli create
18
19
  ```
19
20
 
20
21
  ## Commands
21
22
 
22
23
  | Command | Description |
23
24
  |---------|-------------|
24
- | `create <name>` | Scaffold a new widget project |
25
- | `validate` | Validate widget manifest and bundle |
26
- | `info` | Show widget metadata |
27
- | `publish` | Publish widget to the registry |
25
+ | `create` | Create a new widget project (default when no project found) |
26
+ | `add` | Add a new widget to an existing project |
27
+ | `build` | Build all widgets |
28
+ | `connect <url>` | Connect to a running dashboard for live testing |
29
+ | `validate [name]` | Validate all widgets or a specific one |
30
+ | `publish` | Build and publish to GlassHome Hub |
31
+ | `login [hub-url]` | Authenticate with GlassHome Hub |
32
+ | `info [name]` | Show widget metadata and bundle info |
33
+ | `upgrade` | Upgrade @glasshome/widget-sdk to latest version |
34
+
35
+ Running `glasshome-widget` with no command inside a widget project shows help. Outside a widget project, it starts the create wizard.
28
36
 
29
37
  ## Documentation
30
38
 
package/bin/cli.ts CHANGED
@@ -21,6 +21,7 @@ const HELP = `
21
21
  glasshome-widget <command> [--dir <path>] [options]
22
22
 
23
23
  Commands:
24
+ create Create a new widget project (default when no project found)
24
25
  add Add a new widget to the project
25
26
  build Build all widgets (per-widget, self-contained bundles)
26
27
  connect <url> Connect to a running dashboard for live testing
@@ -32,6 +33,8 @@ Commands:
32
33
  help Show this help message
33
34
 
34
35
  Examples:
36
+ glasshome-widget Create a new widget project
37
+ glasshome-widget create Create a new widget project
35
38
  glasshome-widget add
36
39
  glasshome-widget build
37
40
  glasshome-widget connect http://localhost:3333
@@ -55,14 +58,29 @@ if (dirFlagIdx !== -1) {
55
58
  }
56
59
  const [command, ...args] = rawArgs;
57
60
 
58
- if (!command || command === "help" || command === "--help" || command === "-h") {
61
+ if (command === "help" || command === "--help" || command === "-h") {
59
62
  console.log(HELP);
60
63
  process.exit(0);
61
64
  }
62
65
 
63
- intro(`glasshome-widget ${command}`);
66
+ // No command: if inside a widget project show help, otherwise run create
67
+ const effectiveCommand = command ?? (existsSync(resolve(process.cwd(), "src")) ? "help" : "create");
68
+
69
+ if (effectiveCommand === "help") {
70
+ console.log(HELP);
71
+ process.exit(0);
72
+ }
73
+
74
+ intro(`glasshome-widget ${effectiveCommand}`);
75
+
76
+ switch (effectiveCommand) {
77
+ case "create": {
78
+ const { runCreate } = await import("../src/commands/create");
79
+ await runCreate();
80
+ outro("Happy building!");
81
+ break;
82
+ }
64
83
 
65
- switch (command) {
66
84
  case "add": {
67
85
  const { runAdd } = await import("../src/commands/add");
68
86
  await runAdd(resolveWidgetDir());
package/package.json CHANGED
@@ -1,14 +1,12 @@
1
1
  {
2
2
  "name": "@glasshome/widget-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for creating and managing GlassHome widgets",
5
5
  "type": "module",
6
6
  "bin": {
7
- "create-glasshome-widget": "bin/create.ts",
8
7
  "glasshome-widget": "bin/cli.ts"
9
8
  },
10
9
  "scripts": {
11
- "create": "bun bin/create.ts",
12
10
  "cli": "bun bin/cli.ts"
13
11
  },
14
12
  "files": [
@@ -0,0 +1,211 @@
1
+ import {
2
+ cpSync,
3
+ existsSync,
4
+ readdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ rmSync,
8
+ statSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { cancel, confirm, isCancel, log, spinner, text } from "@clack/prompts";
13
+ import { promptWidgetDetails, scaffoldWidget } from "./add";
14
+
15
+ const templateDir = resolve(import.meta.dir, "../../template");
16
+
17
+ /** Walk up from cwd looking for a package.json with a `workspaces` field. */
18
+ function findMonorepoRoot(from: string): string | null {
19
+ let dir = resolve(from);
20
+ const root = (dir.match(/^[A-Za-z]:\\/) ?? ["/"])[0];
21
+ while (true) {
22
+ const pkgPath = join(dir, "package.json");
23
+ if (existsSync(pkgPath)) {
24
+ try {
25
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
26
+ if (pkg.workspaces) return dir;
27
+ } catch {}
28
+ }
29
+ const parent = dirname(dir);
30
+ if (parent === dir || parent === root) break;
31
+ dir = parent;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ /** Check if cwd is inside an existing git repository. */
37
+ function isInsideGitRepo(from: string): boolean {
38
+ const result = Bun.spawnSync(["git", "rev-parse", "--is-inside-work-tree"], {
39
+ cwd: from,
40
+ stderr: "pipe",
41
+ });
42
+ return result.exitCode === 0;
43
+ }
44
+
45
+ export async function runCreate() {
46
+ const projectName = await text({
47
+ message: "Project name",
48
+ placeholder: "my-widgets",
49
+ defaultValue: "my-widgets",
50
+ validate(value) {
51
+ if (!value) return "Name is required";
52
+ if (/\s/.test(value)) return "Name cannot contain spaces";
53
+ if (!/^[a-z]/.test(value)) return "Name must start with a lowercase letter";
54
+ if (!/^[a-z][a-z0-9-]*$/.test(value))
55
+ return "Name must be lowercase alphanumeric with optional hyphens";
56
+ },
57
+ });
58
+
59
+ if (isCancel(projectName)) {
60
+ cancel("Operation cancelled.");
61
+ process.exit(0);
62
+ }
63
+
64
+ const projectDescription = await text({
65
+ message: "Project description (optional)",
66
+ placeholder: "My GlassHome widget collection",
67
+ defaultValue: "",
68
+ });
69
+
70
+ if (isCancel(projectDescription)) {
71
+ cancel("Operation cancelled.");
72
+ process.exit(0);
73
+ }
74
+
75
+ log.info("Let's set up your first widget:");
76
+
77
+ const widgetDetails = await promptWidgetDetails();
78
+ if (!widgetDetails) process.exit(0);
79
+
80
+ const targetDir = resolve(process.cwd(), projectName as string);
81
+
82
+ if (existsSync(targetDir)) {
83
+ cancel(`Directory "${projectName}" already exists.`);
84
+ process.exit(1);
85
+ }
86
+
87
+ const s = spinner();
88
+ s.start("Scaffolding widget project...");
89
+
90
+ try {
91
+ cpSync(templateDir, targetDir, { recursive: true });
92
+
93
+ // Strip .template suffixes (used to prevent IDE type-checking of template files)
94
+ function processDir(dir: string) {
95
+ for (const entry of readdirSync(dir)) {
96
+ const fullPath = join(dir, entry);
97
+ if (statSync(fullPath).isDirectory()) {
98
+ processDir(fullPath);
99
+ continue;
100
+ }
101
+ if (entry.endsWith(".template")) {
102
+ renameSync(fullPath, join(dir, entry.replace(/\.template$/, "")));
103
+ }
104
+ }
105
+ }
106
+ processDir(targetDir);
107
+
108
+ // Remove the template's src/index.tsx (it will be placed into a widget subdir)
109
+ const oldSrcIndex = join(targetDir, "src/index.tsx");
110
+ if (existsSync(oldSrcIndex)) {
111
+ rmSync(oldSrcIndex);
112
+ }
113
+
114
+ const projectDesc =
115
+ (projectDescription as string) || `A GlassHome widget project: ${projectName}`;
116
+
117
+ // Replace project-level placeholders in package.json
118
+ const pkgPath = join(targetDir, "package.json");
119
+ let pkgContent = readFileSync(pkgPath, "utf-8");
120
+ pkgContent = pkgContent.replace(/PROJECT_NAME/g, projectName as string);
121
+ pkgContent = pkgContent.replace(/PROJECT_DESCRIPTION/g, projectDesc);
122
+ writeFileSync(pkgPath, pkgContent);
123
+
124
+ // Replace project-level placeholders in README.md
125
+ const readmePath = join(targetDir, "README.md");
126
+ let readmeContent = readFileSync(readmePath, "utf-8");
127
+ readmeContent = readmeContent.replace(/PROJECT_NAME/g, projectName as string);
128
+ readmeContent = readmeContent.replace(/PROJECT_DESCRIPTION/g, projectDesc);
129
+ writeFileSync(readmePath, readmeContent);
130
+
131
+ // Create the first widget in src/{widgetName}/
132
+ scaffoldWidget(targetDir, widgetDetails);
133
+
134
+ s.stop("Widget project created!");
135
+
136
+ // Post-scaffold: auto-install
137
+ const monorepoRoot = findMonorepoRoot(process.cwd());
138
+ const installCwd = monorepoRoot ?? targetDir;
139
+ const installHint = monorepoRoot ? " (from monorepo root)" : "";
140
+
141
+ const shouldInstall = await confirm({
142
+ message: `Install dependencies?${installHint}`,
143
+ initialValue: true,
144
+ });
145
+
146
+ if (!isCancel(shouldInstall) && shouldInstall) {
147
+ const installSpinner = spinner();
148
+ installSpinner.start("Installing dependencies...");
149
+ const installProc = Bun.spawnSync(["bun", "install"], { cwd: installCwd });
150
+ if (installProc.exitCode === 0) {
151
+ installSpinner.stop("Dependencies installed");
152
+ } else {
153
+ installSpinner.stop(
154
+ monorepoRoot
155
+ ? "Install failed — run `bun install` from the monorepo root"
156
+ : "Install failed — you can run `bun install` manually",
157
+ );
158
+ }
159
+ }
160
+
161
+ // Post-scaffold: git init (skip when already inside a git repo)
162
+ const inGitRepo = isInsideGitRepo(process.cwd());
163
+
164
+ if (inGitRepo) {
165
+ log.info("Skipping git init — already in a git repository");
166
+ } else {
167
+ const shouldGit = await confirm({
168
+ message: "Initialize git repository?",
169
+ initialValue: true,
170
+ });
171
+
172
+ if (!isCancel(shouldGit) && shouldGit) {
173
+ const gitSpinner = spinner();
174
+ gitSpinner.start("Initializing git repository...");
175
+ const initProc = Bun.spawnSync(["git", "init"], { cwd: targetDir });
176
+ if (initProc.exitCode === 0) {
177
+ Bun.spawnSync(["git", "add", "-A"], { cwd: targetDir });
178
+ Bun.spawnSync(["git", "commit", "-m", "Initial widget scaffold"], { cwd: targetDir });
179
+ gitSpinner.stop("Git repository initialized");
180
+ } else {
181
+ gitSpinner.stop("Git init failed — you can run `git init` manually");
182
+ }
183
+ }
184
+ }
185
+
186
+ // Next-steps guide
187
+ log.info("");
188
+ log.step("Next steps:");
189
+ if (monorepoRoot) {
190
+ log.info(" bun install Run from monorepo root to link workspace packages");
191
+ log.info(` cd ${projectName}`);
192
+ } else {
193
+ log.info(` cd ${projectName}`);
194
+ if (isCancel(shouldInstall) || !shouldInstall) {
195
+ log.info(" bun install");
196
+ }
197
+ }
198
+ log.info(" glasshome-widget connect <url> Connect to a running dashboard for live testing");
199
+ log.info(" glasshome-widget add Add another widget to the project");
200
+ log.info(" glasshome-widget build Build all widgets");
201
+ log.info(" glasshome-widget validate Validate all widgets");
202
+ log.info(" glasshome-widget publish Build & publish to GlassHome Hub");
203
+ log.info(" glasshome-widget info Show widget metadata");
204
+ log.info(" glasshome-widget upgrade Upgrade @glasshome/widget-sdk");
205
+ } catch (err) {
206
+ s.stop("Failed to scaffold widget project.");
207
+ rmSync(targetDir, { recursive: true, force: true });
208
+ cancel(`Error: ${err instanceof Error ? err.message : String(err)}`);
209
+ process.exit(1);
210
+ }
211
+ }
package/bin/create.ts DELETED
@@ -1,220 +0,0 @@
1
- #!/usr/bin/env bun
2
- import {
3
- cpSync,
4
- existsSync,
5
- readdirSync,
6
- readFileSync,
7
- renameSync,
8
- rmSync,
9
- statSync,
10
- writeFileSync,
11
- } from "node:fs";
12
- import { dirname, join, resolve } from "node:path";
13
- import { cancel, confirm, intro, isCancel, log, outro, spinner, text } from "@clack/prompts";
14
- import { promptWidgetDetails, scaffoldWidget } from "../src/commands/add";
15
-
16
- const templateDir = resolve(import.meta.dir, "../template");
17
-
18
- /** Walk up from cwd looking for a package.json with a `workspaces` field. */
19
- function findMonorepoRoot(from: string): string | null {
20
- let dir = resolve(from);
21
- const root = (dir.match(/^[A-Za-z]:\\/) ?? ["/"])[0];
22
- while (true) {
23
- const pkgPath = join(dir, "package.json");
24
- if (existsSync(pkgPath)) {
25
- try {
26
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
27
- if (pkg.workspaces) return dir;
28
- } catch {}
29
- }
30
- const parent = dirname(dir);
31
- if (parent === dir || parent === root) break;
32
- dir = parent;
33
- }
34
- return null;
35
- }
36
-
37
- /** Check if cwd is inside an existing git repository. */
38
- function isInsideGitRepo(from: string): boolean {
39
- const result = Bun.spawnSync(["git", "rev-parse", "--is-inside-work-tree"], {
40
- cwd: from,
41
- stderr: "pipe",
42
- });
43
- return result.exitCode === 0;
44
- }
45
-
46
- intro("Create GlassHome Widget Project");
47
-
48
- // ---- Project-level prompts ----
49
-
50
- const projectName = await text({
51
- message: "Project name",
52
- placeholder: "my-widgets",
53
- defaultValue: "my-widgets",
54
- validate(value) {
55
- if (!value) return "Name is required";
56
- if (/\s/.test(value)) return "Name cannot contain spaces";
57
- if (!/^[a-z]/.test(value)) return "Name must start with a lowercase letter";
58
- if (!/^[a-z][a-z0-9-]*$/.test(value))
59
- return "Name must be lowercase alphanumeric with optional hyphens";
60
- },
61
- });
62
-
63
- if (isCancel(projectName)) {
64
- cancel("Operation cancelled.");
65
- process.exit(0);
66
- }
67
-
68
- const projectDescription = await text({
69
- message: "Project description (optional)",
70
- placeholder: "My GlassHome widget collection",
71
- defaultValue: "",
72
- });
73
-
74
- if (isCancel(projectDescription)) {
75
- cancel("Operation cancelled.");
76
- process.exit(0);
77
- }
78
-
79
- // ---- First widget prompts ----
80
-
81
- log.info("Let's set up your first widget:");
82
-
83
- const widgetDetails = await promptWidgetDetails();
84
- if (!widgetDetails) process.exit(0);
85
-
86
- const targetDir = resolve(process.cwd(), projectName as string);
87
-
88
- if (existsSync(targetDir)) {
89
- cancel(`Directory "${projectName}" already exists.`);
90
- process.exit(1);
91
- }
92
-
93
- const s = spinner();
94
- s.start("Scaffolding widget project...");
95
-
96
- try {
97
- // Copy template to target
98
- cpSync(templateDir, targetDir, { recursive: true });
99
-
100
- // Strip .template suffixes (used to prevent IDE type-checking of template files)
101
- function processDir(dir: string) {
102
- for (const entry of readdirSync(dir)) {
103
- const fullPath = join(dir, entry);
104
- if (statSync(fullPath).isDirectory()) {
105
- processDir(fullPath);
106
- continue;
107
- }
108
- if (entry.endsWith(".template")) {
109
- renameSync(fullPath, join(dir, entry.replace(/\.template$/, "")));
110
- }
111
- }
112
- }
113
- processDir(targetDir);
114
-
115
- // Remove the template's src/index.tsx (it will be placed into a widget subdir)
116
- const oldSrcIndex = join(targetDir, "src/index.tsx");
117
- if (existsSync(oldSrcIndex)) {
118
- rmSync(oldSrcIndex);
119
- }
120
-
121
- const projectDesc =
122
- (projectDescription as string) || `A GlassHome widget project: ${projectName}`;
123
-
124
- // Replace project-level placeholders in package.json
125
- const pkgPath = join(targetDir, "package.json");
126
- let pkgContent = readFileSync(pkgPath, "utf-8");
127
- pkgContent = pkgContent.replace(/PROJECT_NAME/g, projectName as string);
128
- pkgContent = pkgContent.replace(/PROJECT_DESCRIPTION/g, projectDesc);
129
- writeFileSync(pkgPath, pkgContent);
130
-
131
- // Replace project-level placeholders in README.md
132
- const readmePath = join(targetDir, "README.md");
133
- let readmeContent = readFileSync(readmePath, "utf-8");
134
- readmeContent = readmeContent.replace(/PROJECT_NAME/g, projectName as string);
135
- readmeContent = readmeContent.replace(/PROJECT_DESCRIPTION/g, projectDesc);
136
- writeFileSync(readmePath, readmeContent);
137
-
138
- // Create the first widget in src/{widgetName}/
139
- scaffoldWidget(targetDir, widgetDetails);
140
-
141
- s.stop("Widget project created!");
142
-
143
- // Post-scaffold: auto-install
144
- const monorepoRoot = findMonorepoRoot(process.cwd());
145
- const installCwd = monorepoRoot ?? targetDir;
146
- const installHint = monorepoRoot ? " (from monorepo root)" : "";
147
-
148
- const shouldInstall = await confirm({
149
- message: `Install dependencies?${installHint}`,
150
- initialValue: true,
151
- });
152
-
153
- if (!isCancel(shouldInstall) && shouldInstall) {
154
- const installSpinner = spinner();
155
- installSpinner.start("Installing dependencies...");
156
- const installProc = Bun.spawnSync(["bun", "install"], { cwd: installCwd });
157
- if (installProc.exitCode === 0) {
158
- installSpinner.stop("Dependencies installed");
159
- } else {
160
- installSpinner.stop(
161
- monorepoRoot
162
- ? "Install failed — run `bun install` from the monorepo root"
163
- : "Install failed — you can run `bun install` manually",
164
- );
165
- }
166
- }
167
-
168
- // Post-scaffold: git init (skip when already inside a git repo)
169
- const inGitRepo = isInsideGitRepo(process.cwd());
170
-
171
- if (inGitRepo) {
172
- log.info("Skipping git init — already in a git repository");
173
- } else {
174
- const shouldGit = await confirm({
175
- message: "Initialize git repository?",
176
- initialValue: true,
177
- });
178
-
179
- if (!isCancel(shouldGit) && shouldGit) {
180
- const gitSpinner = spinner();
181
- gitSpinner.start("Initializing git repository...");
182
- const initProc = Bun.spawnSync(["git", "init"], { cwd: targetDir });
183
- if (initProc.exitCode === 0) {
184
- Bun.spawnSync(["git", "add", "-A"], { cwd: targetDir });
185
- Bun.spawnSync(["git", "commit", "-m", "Initial widget scaffold"], { cwd: targetDir });
186
- gitSpinner.stop("Git repository initialized");
187
- } else {
188
- gitSpinner.stop("Git init failed — you can run `git init` manually");
189
- }
190
- }
191
- }
192
-
193
- // Enhanced outro with next-steps guide
194
- log.info("");
195
- log.step("Next steps:");
196
- if (monorepoRoot) {
197
- log.info(" bun install Run from monorepo root to link workspace packages");
198
- log.info(` cd ${projectName}`);
199
- } else {
200
- log.info(` cd ${projectName}`);
201
- if (isCancel(shouldInstall) || !shouldInstall) {
202
- log.info(" bun install");
203
- }
204
- }
205
- log.info(" bun widget connect <url> Connect to a running dashboard for live testing");
206
- log.info(" bun widget add Add another widget to the project");
207
- log.info(" bun run build Build all widgets + generate registry.json");
208
- log.info(" bun widget validate Validate all widgets");
209
- log.info(" bun widget publish Build & publish to GlassHome Hub");
210
- log.info(" bun widget info Show widget metadata");
211
- log.info(" bun widget upgrade Upgrade @glasshome/widget-sdk");
212
-
213
- outro("Happy building!");
214
- } catch (err) {
215
- s.stop("Failed to scaffold widget project.");
216
- // Rollback: remove partially created directory
217
- rmSync(targetDir, { recursive: true, force: true });
218
- cancel(`Error: ${err instanceof Error ? err.message : String(err)}`);
219
- process.exit(1);
220
- }