@soloworks/smking-next 0.9.0 → 0.9.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @soloworks/smking-next
2
2
 
3
+ ## 0.9.1 — 2026-05-13
4
+
5
+ **`smking-next doctor` subcommand for self-check + machine-readable output.** The CLI bin now accepts two subcommands: `install` (existing one-shot scaffold) and the new `doctor` (self-check). With `--json`, doctor emits structured output for the new `@smking/wizard` install agent's `run_doctor` tool.
6
+
7
+ ### What's new
8
+
9
+ `npx @soloworks/smking-next doctor` runs four checks:
10
+
11
+ - `SMKING_API_KEY` set (and starts with `pk_`)
12
+ - `SMKING_BASE_URL` set (and starts with `http`)
13
+ - `<SmkingAEO />` imported in the root `app/layout.{tsx,jsx,ts,js}`
14
+ - Live probe of `${SMKING_BASE_URL}/api/v1/public/aeo` (3s timeout)
15
+
16
+ Exit code is `0` when every required check passes, `1` otherwise. Identical between pretty and JSON modes.
17
+
18
+ ### `--json` shape
19
+
20
+ ```json
21
+ {
22
+ "checks": [
23
+ { "name": "...", "status": "pass" | "fail" | "info", "detail": "..." }
24
+ ],
25
+ "summary": { "passed": N, "failed": N, "info": N, "ok": <bool> }
26
+ }
27
+ ```
28
+
29
+ `summary.ok` is the short-circuit boolean for agentic consumers. The shape is a stable wizard contract — schema changes will bump wizard version.
30
+
31
+ ### Why
32
+
33
+ Mirrors `smking/laravel`'s `php artisan smking:doctor --json` (v0.10.1). The `@smking/wizard` CLI now runs the same install verification step for both stacks without needing to scrape ANSI-coloured terminal output.
34
+
35
+ Pure addition — existing `npx @soloworks/smking-next install` calls are unchanged, default behaviour preserved.
36
+
3
37
  ## 0.9.0 — 2026-05-12
4
38
 
5
39
  **Drop-in takeover for `/sitemap.xml`, `/robots.txt`, and `/llms.txt` plus a one-shot install CLI.** Customers with no sitemap (or a broken one), no `robots.txt`, or no `llms.txt` for AI agents previously had to write all three themselves. With v0.9.0 the SDK serves them from the smking SaaS — one line of customer code per file, scaffolded automatically.
package/bin/install.ts CHANGED
@@ -1,18 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `npx @soloworks/smking-next install`
3
+ * `npx @soloworks/smking-next [install|doctor] [--json]`
4
4
  *
5
- * One-shot scaffold for the three takeover drop-in files. Idempotent: if a
6
- * file already exists, we leave it alone and tell the user — never
7
- * overwrite customer code. Each generated file is one line of re-export
8
- * pointing at the SDK helper that does the real work.
5
+ * Two subcommands share this entry point (Node 22+ strips TS at runtime, so
6
+ * the package ships .ts source directly — no build step):
7
+ *
8
+ * - **install** (default) — One-shot scaffold for the three takeover
9
+ * drop-in files (sitemap.ts / robots.ts / llms.txt route). Idempotent:
10
+ * already-existing files are skipped, never overwritten.
11
+ *
12
+ * - **doctor** — Self-check (env presence + <SmkingAEO /> usage + API
13
+ * reachable). `--json` flag emits structured output for the
14
+ * @smking/wizard install agent's `run_doctor` MCP tool.
9
15
  *
10
16
  * Conventional Next.js layout assumed: `app/` at repo root (or under
11
- * `src/app/` — detected). The CLI exits 1 if neither exists.
17
+ * `src/app/` — detected). Both subcommands exit 1 on missing app/.
12
18
  */
13
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
20
  import { join } from "node:path";
15
21
 
22
+ // ── install (既有功能) ──────────────────────────────────────────
23
+
16
24
  interface FileSpec {
17
25
  path: string; // relative to detected app root
18
26
  content: string;
@@ -48,7 +56,7 @@ function detectAppDir(): string | null {
48
56
  return null;
49
57
  }
50
58
 
51
- function main(): number {
59
+ function runInstall(): number {
52
60
  const appDir = detectAppDir();
53
61
  if (!appDir) {
54
62
  console.error(
@@ -82,4 +90,202 @@ function main(): number {
82
90
  return 0;
83
91
  }
84
92
 
85
- process.exit(main());
93
+ // ── doctor (新增) ──────────────────────────────────────────────
94
+
95
+ interface DoctorCheck {
96
+ name: string;
97
+ status: "pass" | "fail" | "info";
98
+ detail: string;
99
+ }
100
+
101
+ function checkEnv(name: string, prefix?: string): DoctorCheck {
102
+ const value = process.env[name];
103
+ if (!value) {
104
+ return {
105
+ name: `${name} set`,
106
+ status: "fail",
107
+ detail: `not set; add ${name}=... to .env.local`,
108
+ };
109
+ }
110
+ if (prefix && !value.startsWith(prefix)) {
111
+ return {
112
+ name: `${name} set`,
113
+ status: "fail",
114
+ detail: `value must start with \`${prefix}\``,
115
+ };
116
+ }
117
+ return {
118
+ name: `${name} set`,
119
+ status: "pass",
120
+ detail: `${value.slice(0, 8)}…`,
121
+ };
122
+ }
123
+
124
+ function checkLayoutUsage(appDir: string): DoctorCheck {
125
+ // Mirror Next.js's layout file resolution. Order matters — TS > JS to
126
+ // match what Next.js itself would pick.
127
+ const candidates = [
128
+ join(appDir, "layout.tsx"),
129
+ join(appDir, "layout.jsx"),
130
+ join(appDir, "layout.ts"),
131
+ join(appDir, "layout.js"),
132
+ ];
133
+ const layoutPath = candidates.find((p) => existsSync(p));
134
+ if (!layoutPath) {
135
+ return {
136
+ name: "<SmkingAEO /> in root layout",
137
+ status: "info",
138
+ detail: `no layout file found under ${appDir}/ — skipped`,
139
+ };
140
+ }
141
+
142
+ const content = readFileSync(layoutPath, "utf-8");
143
+ // Substring check is intentional rather than AST parsing. A customer
144
+ // commenting it out or aliasing the import shows up as fail/info either
145
+ // way, and AST adds a TypeScript parser dependency for trivial value.
146
+ if (!content.includes("SmkingAEO")) {
147
+ return {
148
+ name: "<SmkingAEO /> in root layout",
149
+ status: "fail",
150
+ detail: `${layoutPath} does not import SmkingAEO. Add: import { SmkingAEO } from "@soloworks/smking-next"; then render <SmkingAEO apiKey={process.env.SMKING_API_KEY!} /> inside <body>.`,
151
+ };
152
+ }
153
+ return {
154
+ name: "<SmkingAEO /> in root layout",
155
+ status: "pass",
156
+ detail: layoutPath,
157
+ };
158
+ }
159
+
160
+ async function checkApiReachable(): Promise<DoctorCheck> {
161
+ const apiKey = process.env.SMKING_API_KEY;
162
+ const baseUrl = process.env.SMKING_BASE_URL;
163
+ if (!apiKey || !baseUrl) {
164
+ return {
165
+ name: "API reachable",
166
+ status: "info",
167
+ detail: "skipped — SMKING_API_KEY or SMKING_BASE_URL not set",
168
+ };
169
+ }
170
+
171
+ // Probe the actual AEO endpoint, not the base URL root. Hitting root
172
+ // would pass for any live host (a typo'd domain, google.com); the
173
+ // endpoint either returns the AEO payload or a known auth/validation
174
+ // status — both confirm the route exists.
175
+ const url = `${baseUrl.replace(/\/$/, "")}/api/v1/public/aeo?path=%2F`;
176
+ try {
177
+ const res = await fetch(url, {
178
+ headers: { authorization: `Bearer ${apiKey}` },
179
+ signal: AbortSignal.timeout(3000),
180
+ });
181
+
182
+ if (res.ok) {
183
+ return {
184
+ name: "API reachable",
185
+ status: "pass",
186
+ detail: `${url} → HTTP ${res.status}`,
187
+ };
188
+ }
189
+ if (res.status === 401) {
190
+ return {
191
+ name: "API reachable",
192
+ status: "fail",
193
+ detail: `HTTP 401 from ${url} — SMKING_API_KEY rejected`,
194
+ };
195
+ }
196
+ if (res.status === 404) {
197
+ return {
198
+ name: "API reachable",
199
+ status: "fail",
200
+ detail: `HTTP 404 from ${url} — base_url likely points at the wrong host`,
201
+ };
202
+ }
203
+ if (res.status >= 500) {
204
+ return {
205
+ name: "API reachable",
206
+ status: "fail",
207
+ detail: `upstream HTTP ${res.status} from ${url}`,
208
+ };
209
+ }
210
+ // 4xx (other than 401/404) — likely validation error, endpoint exists
211
+ return {
212
+ name: "API reachable",
213
+ status: "info",
214
+ detail: `${url} → HTTP ${res.status} (endpoint exists)`,
215
+ };
216
+ } catch (err) {
217
+ return {
218
+ name: "API reachable",
219
+ status: "fail",
220
+ detail: `connection failed: ${err instanceof Error ? err.message : String(err)}`,
221
+ };
222
+ }
223
+ }
224
+
225
+ async function runDoctor(jsonOutput: boolean): Promise<number> {
226
+ const appDir = detectAppDir();
227
+ const checks: DoctorCheck[] = [
228
+ checkEnv("SMKING_API_KEY", "pk_"),
229
+ checkEnv("SMKING_BASE_URL", "http"),
230
+ appDir
231
+ ? checkLayoutUsage(appDir)
232
+ : {
233
+ name: "<SmkingAEO /> in root layout",
234
+ status: "info" as const,
235
+ detail: "no app/ or src/app/ directory found — skipped",
236
+ },
237
+ await checkApiReachable(),
238
+ ];
239
+
240
+ const hasFailure = checks.some((c) => c.status === "fail");
241
+
242
+ // JSON mode: parseable structured output for @smking/wizard's run_doctor
243
+ // tool. Stable shape — do not break without bumping wizard version.
244
+ if (jsonOutput) {
245
+ const summary = {
246
+ passed: checks.filter((c) => c.status === "pass").length,
247
+ failed: checks.filter((c) => c.status === "fail").length,
248
+ info: checks.filter((c) => c.status === "info").length,
249
+ ok: !hasFailure,
250
+ };
251
+ console.log(JSON.stringify({ checks, summary }));
252
+ return hasFailure ? 1 : 0;
253
+ }
254
+
255
+ for (const check of checks) {
256
+ const icon =
257
+ check.status === "pass"
258
+ ? "✅"
259
+ : check.status === "fail"
260
+ ? "❌"
261
+ : "ℹ️ ";
262
+ console.log(`${icon} ${check.name} — ${check.detail}`);
263
+ }
264
+ console.log();
265
+ if (hasFailure) {
266
+ console.log("❌ smking: install incomplete — fix the items above.");
267
+ return 1;
268
+ }
269
+ console.log("✅ smking: install OK.");
270
+ return 0;
271
+ }
272
+
273
+ // ── Entry dispatcher ────────────────────────────────────────────
274
+
275
+ async function main(): Promise<number> {
276
+ const subcommand = process.argv[2];
277
+ const jsonOutput = process.argv.includes("--json");
278
+
279
+ if (subcommand === "doctor") {
280
+ return runDoctor(jsonOutput);
281
+ }
282
+ if (subcommand === "install" || subcommand === undefined) {
283
+ return runInstall();
284
+ }
285
+ console.error(
286
+ `Unknown subcommand: ${subcommand}. Available: install (default), doctor.`,
287
+ );
288
+ return 1;
289
+ }
290
+
291
+ main().then((code) => process.exit(code));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soloworks/smking-next",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "AI-native SEO (AEO) for Next.js — auto-inject JSON-LD, FAQ, AI summary, and SEO metadata so AI crawlers (ChatGPT, Perplexity, Google AI) can cite your pages.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/sillyleo/smking/tree/main/packages/smking-next",