@inovus-medical/docs 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.
Files changed (3) hide show
  1. package/bin/cli.js +25 -1
  2. package/package.json +1 -1
  3. package/src/init.js +173 -0
package/bin/cli.js CHANGED
@@ -3,6 +3,7 @@ import path from 'node:path'
3
3
  import { build } from '../src/build.js'
4
4
  import { serve } from '../src/serve.js'
5
5
  import { watchAndRebuild } from '../src/watch.js'
6
+ import { init } from '../src/init.js'
6
7
 
7
8
  function parseFlags(args) {
8
9
  const opts = {}
@@ -32,6 +33,22 @@ const outDir = flags.out || 'dist'
32
33
  const port = Number(flags.port || 8788)
33
34
 
34
35
  async function run() {
36
+ if (cmd === 'init') {
37
+ const { created, skipped, name, title, visibility, repoUrl, home } = await init({ root, flags })
38
+ console.log(`Scaffolded "${title}" (worker: ${name}, ${visibility})${repoUrl ? ` — ${repoUrl}` : ''}`)
39
+ if (created.length) console.log(' created: ' + created.join(', '))
40
+ if (skipped.length) console.log(' kept existing (use --force to overwrite): ' + skipped.join(', '))
41
+ if (!home) {
42
+ console.log(' ⚠ No home page (README.md / index.md) and --no-readme set — the site will have no page at /.')
43
+ }
44
+ console.log('\nNext:')
45
+ console.log(' npm install')
46
+ console.log(' npm run dev # preview at http://localhost:8788')
47
+ console.log(' git add -A && git commit -m "Add docs site config" && git push')
48
+ console.log(' then connect the repo in Cloudflare (build: npm install && npm run build)')
49
+ return
50
+ }
51
+
35
52
  if (cmd === 'build') {
36
53
  const { pageCount, outPath, contentDir } = await build({ root, inDir, outDir })
37
54
  console.log(`✓ Built ${pageCount} page(s) from ${contentDir === '.' ? 'repo root' : contentDir + '/'} -> ${path.relative(process.cwd(), outPath) || outPath}`)
@@ -54,7 +71,14 @@ async function run() {
54
71
  return
55
72
  }
56
73
 
57
- console.error(`Unknown command: ${cmd}\nUsage: inovus-docs <build|dev|serve> [--root .] [--in docs] [--out dist] [--port 8788]`)
74
+ console.error(
75
+ `Unknown command: ${cmd}\n` +
76
+ `Usage:\n` +
77
+ ` inovus-docs init [--name <n>] [--title <t>] [--public] [--repo <url>] [--no-readme] [--force]\n` +
78
+ ` inovus-docs build [--root .] [--in <dir>] [--out dist]\n` +
79
+ ` inovus-docs dev [--port 8788]\n` +
80
+ ` inovus-docs serve [--port 8788]`
81
+ )
58
82
  process.exit(1)
59
83
  }
60
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inovus-medical/docs",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Turn a folder of markdown into a themed, Cloudflare-ready docs site.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/init.js ADDED
@@ -0,0 +1,173 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { execSync } from 'node:child_process'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ async function fileExists(p) {
9
+ try { await fs.access(p); return true } catch { return false }
10
+ }
11
+
12
+ async function readJsonIfExists(file) {
13
+ try { return JSON.parse(await fs.readFile(file, 'utf8')) } catch (e) {
14
+ if (e.code === 'ENOENT') return null
15
+ throw e
16
+ }
17
+ }
18
+
19
+ function gitRemote(root) {
20
+ try {
21
+ return execSync('git config --get remote.origin.url', {
22
+ cwd: root,
23
+ stdio: ['ignore', 'pipe', 'ignore']
24
+ }).toString().trim() || null
25
+ } catch {
26
+ return null
27
+ }
28
+ }
29
+
30
+ function parseRemote(url) {
31
+ if (!url) return {}
32
+ const m = url.match(/[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/)
33
+ if (!m) return {}
34
+ const [, org, repo] = m
35
+ let httpsUrl
36
+ if (url.startsWith('git@')) {
37
+ const hm = url.match(/git@([^:]+):(.+?)(?:\.git)?$/)
38
+ httpsUrl = hm ? `https://${hm[1]}/${hm[2]}` : null
39
+ } else {
40
+ httpsUrl = url.replace(/\.git$/, '')
41
+ }
42
+ return { org, repo, httpsUrl }
43
+ }
44
+
45
+ function sanitizeName(s) {
46
+ return (
47
+ String(s).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 54) ||
48
+ 'docs-site'
49
+ )
50
+ }
51
+
52
+ function titleCase(s) {
53
+ return String(s).replace(/[-_]/g, ' ').replace(/\s+/g, ' ').trim().replace(/\b\w/g, (c) => c.toUpperCase())
54
+ }
55
+
56
+ const WRANGLER = (name) => `{
57
+ "$schema": "node_modules/wrangler/config-schema.json",
58
+ "name": "${name}",
59
+ "compatibility_date": "2025-06-01",
60
+ "assets": {
61
+ "directory": "./dist",
62
+ "html_handling": "auto-trailing-slash",
63
+ "not_found_handling": "404-page"
64
+ }
65
+ }
66
+ `
67
+
68
+ async function ensureGitignore(root, created) {
69
+ const file = path.join(root, '.gitignore')
70
+ let body = ''
71
+ try { body = await fs.readFile(file, 'utf8') } catch {}
72
+ const present = new Set(body.split(/\r?\n/).map((l) => l.trim()))
73
+ let next = body
74
+ let changed = false
75
+ for (const entry of ['node_modules/', 'dist/']) {
76
+ if (!present.has(entry)) {
77
+ next += (next && !next.endsWith('\n') ? '\n' : '') + entry + '\n'
78
+ changed = true
79
+ }
80
+ }
81
+ if (changed) {
82
+ await fs.writeFile(file, next)
83
+ created.push(body ? '.gitignore (updated)' : '.gitignore')
84
+ }
85
+ }
86
+
87
+ async function hasHomePage(root) {
88
+ const candidates = ['README.md', 'readme.md', 'index.md', 'docs/README.md', 'docs/index.md']
89
+ for (const c of candidates) if (await fileExists(path.join(root, c))) return true
90
+ return false
91
+ }
92
+
93
+ function starterReadme(title) {
94
+ return `# ${title}
95
+
96
+ Welcome to the ${title} documentation.
97
+
98
+ This is the home page of your docs site. Edit this \`README.md\` to change it, and add
99
+ more \`.md\` files anywhere in the repo — they show up in the sidebar automatically.
100
+ `
101
+ }
102
+
103
+ /** Scaffold the per-repo files needed to publish a markdown repo as a docs site. */
104
+ export async function init({ root = process.cwd(), flags = {} } = {}) {
105
+ const enginePkg = JSON.parse(await fs.readFile(path.join(__dirname, '..', 'package.json'), 'utf8'))
106
+ const engineVersion = `^${enginePkg.version}`
107
+
108
+ const remote = parseRemote(gitRemote(root))
109
+ const folder = path.basename(root)
110
+ const name = sanitizeName(flags.name || remote.repo || folder)
111
+ const title = flags.title || titleCase(remote.repo || folder)
112
+ const visibility = flags.public ? 'public' : 'private'
113
+ const repoUrl = flags.repo || remote.httpsUrl || null
114
+ const force = flags.force === true
115
+
116
+ const created = []
117
+ const skipped = []
118
+
119
+ // docs.config.json (don't clobber existing unless --force)
120
+ const cfgPath = path.join(root, 'docs.config.json')
121
+ if ((await fileExists(cfgPath)) && !force) {
122
+ skipped.push('docs.config.json')
123
+ } else {
124
+ const cfg = { title, description: '', visibility }
125
+ if (repoUrl) cfg.repo = repoUrl
126
+ await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
127
+ created.push('docs.config.json')
128
+ }
129
+
130
+ // wrangler.jsonc
131
+ const wranglerPath = path.join(root, 'wrangler.jsonc')
132
+ if ((await fileExists(wranglerPath)) && !force) {
133
+ skipped.push('wrangler.jsonc')
134
+ } else {
135
+ await fs.writeFile(wranglerPath, WRANGLER(name))
136
+ created.push('wrangler.jsonc')
137
+ }
138
+
139
+ // package.json (merge if present — never remove existing fields)
140
+ const pkgPath = path.join(root, 'package.json')
141
+ const existingPkg = await readJsonIfExists(pkgPath)
142
+ const pkg = existingPkg || { name, private: true }
143
+ pkg.scripts = pkg.scripts || {}
144
+ if (!pkg.scripts.build) pkg.scripts.build = 'inovus-docs build'
145
+ if (!pkg.scripts.dev) pkg.scripts.dev = 'inovus-docs dev'
146
+ pkg.devDependencies = pkg.devDependencies || {}
147
+ pkg.devDependencies[enginePkg.name] = engineVersion
148
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
149
+ created.push(existingPkg ? 'package.json (updated)' : 'package.json')
150
+
151
+ // .node-version for Cloudflare's build image (Wrangler needs Node 22)
152
+ const nodeVerPath = path.join(root, '.node-version')
153
+ if (!(await fileExists(nodeVerPath))) {
154
+ await fs.writeFile(nodeVerPath, '22\n')
155
+ created.push('.node-version')
156
+ }
157
+
158
+ await ensureGitignore(root, created)
159
+
160
+ // Guarantee a landing page: if the repo has no README/index anywhere, drop a starter.
161
+ let home = await hasHomePage(root)
162
+ if (!home && !flags['no-readme']) {
163
+ const inDocsMode = await fileExists(path.join(root, 'docs'))
164
+ const rel = inDocsMode ? path.join('docs', 'README.md') : 'README.md'
165
+ const readmePath = path.join(root, rel)
166
+ await fs.mkdir(path.dirname(readmePath), { recursive: true })
167
+ await fs.writeFile(readmePath, starterReadme(title))
168
+ created.push(rel.split(path.sep).join('/'))
169
+ home = true
170
+ }
171
+
172
+ return { created, skipped, name, title, visibility, repoUrl, home }
173
+ }