@glasshome/widget-cli 0.4.6 → 0.4.9

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/bin/cli.ts CHANGED
@@ -43,22 +43,33 @@ Examples:
43
43
  glasshome-widget info
44
44
  glasshome-widget info clock
45
45
  glasshome-widget publish
46
+ glasshome-widget publish --name light --bump patch
47
+ glasshome-widget publish --name light --bump minor --scope glasshome
46
48
  glasshome-widget login
47
49
  glasshome-widget login https://my-hub.example.com
48
50
  glasshome-widget upgrade
49
51
  glasshome-widget publish --dir packages/widgets
50
52
  `.trim();
51
53
 
52
- const rawArgs = process.argv.slice(2);
53
- const dirFlagIdx = rawArgs.indexOf("--dir");
54
- let explicitDir: string | undefined;
55
- if (dirFlagIdx !== -1) {
56
- explicitDir = rawArgs[dirFlagIdx + 1];
57
- rawArgs.splice(dirFlagIdx, 2);
58
- }
59
- const [command, ...args] = rawArgs;
60
-
61
- if (command === "help" || command === "--help" || command === "-h") {
54
+ import { parseArgs } from "node:util";
55
+
56
+ const { values: flags, positionals } = parseArgs({
57
+ args: process.argv.slice(2),
58
+ options: {
59
+ dir: { type: "string" },
60
+ name: { type: "string" },
61
+ bump: { type: "string" },
62
+ scope: { type: "string" },
63
+ help: { type: "boolean", short: "h" },
64
+ },
65
+ allowPositionals: true,
66
+ strict: false,
67
+ });
68
+
69
+ const explicitDir = flags.dir;
70
+ const [command, ...args] = positionals;
71
+
72
+ if (command === "help" || flags.help) {
62
73
  console.log(HELP);
63
74
  process.exit(0);
64
75
  }
@@ -132,7 +143,11 @@ switch (effectiveCommand) {
132
143
  case "publish": {
133
144
  const hubUrlArg = args[0];
134
145
  const { runPublish } = await import("../src/commands/publish");
135
- await runPublish(resolveWidgetDir(), hubUrlArg);
146
+ await runPublish(resolveWidgetDir(), hubUrlArg, {
147
+ name: flags.name,
148
+ bump: flags.bump as "keep" | "patch" | "minor" | "major" | undefined,
149
+ scope: flags.scope,
150
+ });
136
151
  outro("Done");
137
152
  break;
138
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/widget-cli",
3
- "version": "0.4.6",
3
+ "version": "0.4.9",
4
4
  "description": "CLI for creating and managing GlassHome widgets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@clack/prompts": "^0.10.0",
24
- "@glasshome/widget-sdk": "0.2.3",
24
+ "@glasshome/widget-sdk": "^0.4.0",
25
25
  "open": "^11.0.0",
26
26
  "semver": "^7.6.0",
27
27
  "vite-plugin-solid": "^2.11.0"
@@ -3,7 +3,7 @@ import { resolve } from "node:path";
3
3
  import { log, spinner } from "@clack/prompts";
4
4
  import { buildWidgets } from "@glasshome/widget-sdk/vite";
5
5
  import { trpcMutate, trpcQuery } from "../utils/api";
6
- import { extractHost, getHostToken, storeHostToken } from "../utils/auth";
6
+ import { clearHostToken, extractHost, getHostToken, storeHostToken } from "../utils/auth";
7
7
 
8
8
  interface RegistryWidget {
9
9
  name: string;
@@ -127,9 +127,16 @@ export async function runConnect(apiUrl: string, cwd: string): Promise<void> {
127
127
  headers: { Authorization: `Bearer ${existingToken}` },
128
128
  });
129
129
  if (check.ok) {
130
- token = existingToken;
131
- log.info("Using stored credentials for dashboard.");
130
+ const body = (await check.json()) as { session?: unknown } | null;
131
+ if (body?.session) {
132
+ token = existingToken;
133
+ log.info("Using stored credentials for dashboard.");
134
+ } else {
135
+ clearHostToken(host);
136
+ log.warn("Stored credentials expired — re-authenticating.");
137
+ }
132
138
  } else {
139
+ clearHostToken(host);
133
140
  log.warn("Stored credentials expired — re-authenticating.");
134
141
  }
135
142
  } catch {
@@ -14,7 +14,17 @@ import {
14
14
  } from "../utils/manifest";
15
15
  import { runLogin } from "./login";
16
16
 
17
- export async function runPublish(cwd: string, hubUrlOverride?: string): Promise<void> {
17
+ interface PublishOptions {
18
+ name?: string;
19
+ bump?: "keep" | "patch" | "minor" | "major";
20
+ scope?: string;
21
+ }
22
+
23
+ export async function runPublish(
24
+ cwd: string,
25
+ hubUrlOverride?: string,
26
+ options?: PublishOptions,
27
+ ): Promise<void> {
18
28
  const s = spinner();
19
29
 
20
30
  // Step 1: Validate (quiet)
@@ -53,7 +63,15 @@ export async function runPublish(cwd: string, hubUrlOverride?: string): Promise<
53
63
  }
54
64
 
55
65
  let scope: string;
56
- if (scopes.length === 1) {
66
+ if (options?.scope) {
67
+ const match = scopes.find((s) => s.name === options.scope);
68
+ if (!match) {
69
+ log.error(`Scope @${options.scope} not found. Available: ${scopes.map((s) => s.name).join(", ")}`);
70
+ process.exit(1);
71
+ }
72
+ scope = match.name;
73
+ log.info(`Publishing as @${scope} (${match.type})`);
74
+ } else if (scopes.length === 1) {
57
75
  const only = scopes[0]!;
58
76
  scope = only.name;
59
77
  log.info(`Publishing as @${scope} (${only.type})`);
@@ -76,48 +94,65 @@ export async function runPublish(cwd: string, hubUrlOverride?: string): Promise<
76
94
 
77
95
  // Step 4: Select widget
78
96
  const widgets = discoverWidgets(cwd);
79
- const widgetOptions = widgets.map((name) => {
80
- const manifest = readManifest(cwd, name);
81
- let sizeInfo = "";
82
- try {
83
- const info = getWidgetBundleInfo(cwd, name);
84
- sizeInfo = ` — ${formatBytes(info.raw)}`;
85
- } catch {}
86
- return {
87
- value: name,
88
- label: `${manifest.name}${sizeInfo}`,
89
- };
90
- });
91
-
92
- const selected = await select({
93
- message: "Select widget to publish:",
94
- options: widgetOptions,
95
- });
96
-
97
- if (isCancel(selected)) {
98
- cancel("Publish cancelled.");
99
- process.exit(0);
97
+
98
+ let widgetName: string;
99
+ if (options?.name) {
100
+ if (!widgets.includes(options.name)) {
101
+ log.error(`Widget "${options.name}" not found. Available: ${widgets.join(", ")}`);
102
+ process.exit(1);
103
+ }
104
+ widgetName = options.name;
105
+ } else {
106
+ const widgetOptions = widgets.map((name) => {
107
+ const manifest = readManifest(cwd, name);
108
+ let sizeInfo = "";
109
+ try {
110
+ const info = getWidgetBundleInfo(cwd, name);
111
+ sizeInfo = ` — ${formatBytes(info.raw)}`;
112
+ } catch {}
113
+ return {
114
+ value: name,
115
+ label: `${manifest.name}${sizeInfo}`,
116
+ };
117
+ });
118
+
119
+ const selected = await select({
120
+ message: "Select widget to publish:",
121
+ options: widgetOptions,
122
+ });
123
+
124
+ if (isCancel(selected)) {
125
+ cancel("Publish cancelled.");
126
+ process.exit(0);
127
+ }
128
+ widgetName = selected as string;
100
129
  }
101
130
 
102
- const widgetName = selected as string;
103
131
  const manifest = readManifest(cwd, widgetName);
104
132
 
105
133
  // Step 5: Version bump (per-widget, from manifest)
106
134
  const currentVersion = manifest.version ?? "0.0.0";
107
135
 
108
- const bump = await select({
109
- message: `${manifest.name} version: ${currentVersion}. Bump?`,
110
- options: [
111
- { value: "keep", label: `Keep ${currentVersion}`, hint: "fails if already published" },
112
- { value: "patch", label: `Patch (${semver.inc(currentVersion, "patch")})` },
113
- { value: "minor", label: `Minor (${semver.inc(currentVersion, "minor")})` },
114
- { value: "major", label: `Major (${semver.inc(currentVersion, "major")})` },
115
- ],
116
- });
117
-
118
- if (isCancel(bump)) {
119
- cancel("Publish cancelled.");
120
- process.exit(0);
136
+ let bump: string;
137
+ if (options?.bump) {
138
+ bump = options.bump;
139
+ log.info(`Version: ${currentVersion} → ${bump === "keep" ? currentVersion : semver.inc(currentVersion, bump as semver.ReleaseType) ?? currentVersion}`);
140
+ } else {
141
+ const choice = await select({
142
+ message: `${manifest.name} version: ${currentVersion}. Bump?`,
143
+ options: [
144
+ { value: "keep", label: `Keep ${currentVersion}`, hint: "fails if already published" },
145
+ { value: "patch", label: `Patch (${semver.inc(currentVersion, "patch")})` },
146
+ { value: "minor", label: `Minor (${semver.inc(currentVersion, "minor")})` },
147
+ { value: "major", label: `Major (${semver.inc(currentVersion, "major")})` },
148
+ ],
149
+ });
150
+
151
+ if (isCancel(choice)) {
152
+ cancel("Publish cancelled.");
153
+ process.exit(0);
154
+ }
155
+ bump = choice as string;
121
156
  }
122
157
 
123
158
  let version = currentVersion;
@@ -32,6 +32,16 @@ function validateManifest(manifest: WidgetManifest): ValidationResult {
32
32
  if (!manifest.name) errors.push("Missing required field: name");
33
33
  if (!manifest.sdkVersion) errors.push("Missing required field: sdkVersion");
34
34
 
35
+ // INS-05: "*" sdkVersion is rejected at publish time — authors must declare
36
+ // a real range (e.g. "^0.3.0"). Installed widgets that already ship "*" are
37
+ // still tolerated by the loader with a warn-and-mount, but new publishes
38
+ // are blocked here.
39
+ if (manifest.sdkVersion === "*") {
40
+ errors.push(
41
+ `sdkVersion "*" is not allowed — declare a semver range (e.g. "^0.3.0") to opt into host compatibility checks`,
42
+ );
43
+ }
44
+
35
45
  // Size validation
36
46
  if (!hasValidMinSize) {
37
47
  errors.push("minSize must be an object with numeric w and h properties");