uniweb 1.0.0 → 1.0.2

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 (129) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +532 -3
  3. package/package.json +60 -11
  4. package/partials/adding-pages.hbs +77 -0
  5. package/partials/agents.md +3000 -0
  6. package/partials/ai-assistance.hbs +28 -0
  7. package/partials/components-docs.hbs +13 -0
  8. package/partials/config-reference.hbs +66 -0
  9. package/partials/deployment.hbs +13 -0
  10. package/partials/learn-more.hbs +6 -0
  11. package/partials/quick-start.hbs +8 -0
  12. package/partials/search-docs.hbs +110 -0
  13. package/src/backend/asset-download.js +222 -0
  14. package/src/backend/client.js +986 -0
  15. package/src/backend/foundation-bring-along.js +692 -0
  16. package/src/backend/payment-handoff.js +362 -0
  17. package/src/backend/service-request.js +210 -0
  18. package/src/backend/site-media.js +255 -0
  19. package/src/backend/site-preview.js +77 -0
  20. package/src/backend/site-sync.js +2515 -0
  21. package/src/backend/workspace.js +234 -0
  22. package/src/commands/add.js +1900 -0
  23. package/src/commands/build.js +1214 -0
  24. package/src/commands/clone.js +466 -0
  25. package/src/commands/content.js +202 -0
  26. package/src/commands/deploy.js +537 -0
  27. package/src/commands/dev.js +202 -0
  28. package/src/commands/docs.js +530 -0
  29. package/src/commands/doctor.js +1982 -0
  30. package/src/commands/export.js +116 -0
  31. package/src/commands/families.js +82 -0
  32. package/src/commands/forget.js +197 -0
  33. package/src/commands/handoff.js +21 -0
  34. package/src/commands/i18n.js +1707 -0
  35. package/src/commands/inspect.js +375 -0
  36. package/src/commands/invite.js +24 -0
  37. package/src/commands/org.js +96 -0
  38. package/src/commands/publish.js +1087 -0
  39. package/src/commands/pull.js +1257 -0
  40. package/src/commands/push.js +663 -0
  41. package/src/commands/refresh.js +212 -0
  42. package/src/commands/register.js +1092 -0
  43. package/src/commands/rename.js +647 -0
  44. package/src/commands/site.js +358 -0
  45. package/src/commands/snapshot.js +461 -0
  46. package/src/commands/starter.js +206 -0
  47. package/src/commands/status.js +302 -0
  48. package/src/commands/sync.js +93 -0
  49. package/src/commands/template.js +107 -0
  50. package/src/commands/update.js +887 -0
  51. package/src/commands/validate.js +376 -0
  52. package/src/framework-index.json +248 -0
  53. package/src/index.js +2241 -0
  54. package/src/templates/fetchers/github.js +113 -0
  55. package/src/templates/fetchers/npm.js +85 -0
  56. package/src/templates/fetchers/release.js +92 -0
  57. package/src/templates/index.js +190 -0
  58. package/src/templates/processor.js +333 -0
  59. package/src/templates/resolver.js +232 -0
  60. package/src/templates/validator.js +155 -0
  61. package/src/utils/agents-stamp.js +110 -0
  62. package/src/utils/args.js +126 -0
  63. package/src/utils/asset-upload.js +270 -0
  64. package/src/utils/bytes.js +30 -0
  65. package/src/utils/code-upload.js +394 -0
  66. package/src/utils/config.js +417 -0
  67. package/src/utils/conformance.js +274 -0
  68. package/src/utils/dep-survey.js +176 -0
  69. package/src/utils/destination-prompt.js +146 -0
  70. package/src/utils/discover.js +75 -0
  71. package/src/utils/env.js +22 -0
  72. package/src/utils/exit.js +30 -0
  73. package/src/utils/fetch-retry.js +109 -0
  74. package/src/utils/flag-guard.js +246 -0
  75. package/src/utils/foundation-name.js +179 -0
  76. package/src/utils/git.js +342 -0
  77. package/src/utils/host-prompt.js +51 -0
  78. package/src/utils/install-integrity.js +210 -0
  79. package/src/utils/interactive.js +118 -0
  80. package/src/utils/json-file.js +72 -0
  81. package/src/utils/names.js +91 -0
  82. package/src/utils/placement.js +151 -0
  83. package/src/utils/pm.js +165 -0
  84. package/src/utils/preview.js +20 -0
  85. package/src/utils/pull-written.js +132 -0
  86. package/src/utils/records-guard.js +88 -0
  87. package/src/utils/registry-auth.js +994 -0
  88. package/src/utils/registry-orgs.js +290 -0
  89. package/src/utils/scaffold.js +483 -0
  90. package/src/utils/schemaless-report.js +30 -0
  91. package/src/utils/semver-precedence.js +80 -0
  92. package/src/utils/session-file.js +95 -0
  93. package/src/utils/site-content-refs.js +23 -0
  94. package/src/utils/site-copies.js +123 -0
  95. package/src/utils/site-data-upload.js +302 -0
  96. package/src/utils/site-identity.js +137 -0
  97. package/src/utils/snapshot-settings.js +112 -0
  98. package/src/utils/update-check.js +302 -0
  99. package/src/utils/uwx-read.js +157 -0
  100. package/src/utils/workspace.js +223 -0
  101. package/src/versions.js +467 -0
  102. package/starter/foundation/main.js +50 -0
  103. package/starter/foundation/sections/Section/index.jsx +95 -0
  104. package/starter/foundation/sections/Section/meta.js +44 -0
  105. package/starter/site/pages/about/1-about.md.hbs +13 -0
  106. package/starter/site/pages/about/page.yml +3 -0
  107. package/starter/site/pages/home/1-welcome.md.hbs +14 -0
  108. package/starter/site/pages/home/page.yml +3 -0
  109. package/templates/foundation/_gitignore +5 -0
  110. package/templates/foundation/components/.gitkeep +0 -0
  111. package/templates/foundation/main.js.hbs +33 -0
  112. package/templates/foundation/package.json.hbs +51 -0
  113. package/templates/foundation/sections/.gitkeep +0 -0
  114. package/templates/foundation/styles.css +6 -0
  115. package/templates/foundation/vite.config.js +3 -0
  116. package/templates/site/_gitignore +12 -0
  117. package/templates/site/entry.js +7 -0
  118. package/templates/site/index.html.hbs +13 -0
  119. package/templates/site/package.json.hbs +27 -0
  120. package/templates/site/public/favicon.svg +7 -0
  121. package/templates/site/site.yml.hbs +91 -0
  122. package/templates/site/theme.yml +99 -0
  123. package/templates/site/vite.config.js +7 -0
  124. package/templates/workspace/AGENTS.md.hbs +2 -0
  125. package/templates/workspace/README.md.hbs +87 -0
  126. package/templates/workspace/_gitignore +29 -0
  127. package/templates/workspace/_vscode/settings.json +6 -0
  128. package/templates/workspace/package.json.hbs +20 -0
  129. package/templates/workspace/pnpm-workspace.yaml.hbs +26 -0
@@ -0,0 +1,375 @@
1
+ /**
2
+ * uniweb inspect — Show parsed content shape of markdown files.
3
+ *
4
+ * Usage:
5
+ * uniweb inspect pages/home/hero.md Single section
6
+ * uniweb inspect pages/home/ All sections in a page folder
7
+ * uniweb inspect pages/home/hero.md --raw ProseMirror AST instead of flat shape
8
+ * uniweb inspect pages/home/ --full Include empty fields (matches runtime)
9
+ * uniweb inspect pages/home/ --sequence Include sequence array
10
+ */
11
+
12
+ import { readFileSync, readdirSync, statSync } from 'node:fs'
13
+ import { resolve, extname, basename } from 'node:path'
14
+ import yaml from 'js-yaml'
15
+
16
+ // Colors for terminal output
17
+ const colors = {
18
+ reset: '\x1b[0m',
19
+ red: '\x1b[31m',
20
+ yellow: '\x1b[33m',
21
+ dim: '\x1b[2m'
22
+ }
23
+
24
+ /**
25
+ * Parse CLI arguments for inspect command
26
+ */
27
+ function parseArgs(args) {
28
+ const flags = {
29
+ target: null,
30
+ raw: false,
31
+ full: false,
32
+ sequence: false,
33
+ help: false
34
+ }
35
+
36
+ for (const arg of args) {
37
+ if (arg === '--raw') flags.raw = true
38
+ else if (arg === '--full') flags.full = true
39
+ else if (arg === '--sequence') flags.sequence = true
40
+ else if (arg === '--help' || arg === '-h') flags.help = true
41
+ else if (!arg.startsWith('-')) flags.target = arg
42
+ }
43
+
44
+ return flags
45
+ }
46
+
47
+ /**
48
+ * Dynamically import content-reader and semantic-parser.
49
+ * These are transitive deps of @uniweb/build, available in any project workspace.
50
+ */
51
+ async function loadDependencies() {
52
+ try {
53
+ const [contentReader, semanticParser] = await Promise.all([
54
+ import('@uniweb/content-reader'),
55
+ import('@uniweb/semantic-parser')
56
+ ])
57
+ // Optional: the inset lift a Block runs (`@uniweb/core/insets`). An older
58
+ // install without it shows the insets unlifted, as `inset_ref` nodes.
59
+ const coreInsets = await import('@uniweb/core/insets').catch(() => null)
60
+ return {
61
+ markdownToProseMirror: contentReader.markdownToProseMirror,
62
+ parseContent: semanticParser.parseContent,
63
+ liftInsets: coreInsets?.liftInsets,
64
+ // Optional: present from @uniweb/semantic-parser >= the staircase
65
+ // release; older installs simply show no findings.
66
+ lintContent: semanticParser.lintContent
67
+ }
68
+ } catch {
69
+ console.error(
70
+ `${colors.red}✗${colors.reset} Could not load @uniweb/content-reader or @uniweb/semantic-parser.`
71
+ )
72
+ console.error(
73
+ ` These packages must be installed in the workspace (they come with @uniweb/build).`
74
+ )
75
+ process.exit(1)
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Extract frontmatter and markdown body from a section file.
81
+ * Exact match of content-collector lines 634-639.
82
+ */
83
+ function extractFrontmatter(content) {
84
+ let frontMatter = {}
85
+ let markdown = content
86
+
87
+ if (content.trim().startsWith('---')) {
88
+ const parts = content.split('---\n')
89
+ if (parts.length >= 3) {
90
+ try {
91
+ frontMatter = yaml.load(parts[1]) || {}
92
+ } catch (err) {
93
+ console.warn(
94
+ `${colors.yellow}Warning: YAML parse error: ${err.message}${colors.reset}`
95
+ )
96
+ frontMatter = {}
97
+ }
98
+ markdown = parts.slice(2).join('---\n')
99
+ }
100
+ }
101
+
102
+ return { frontMatter, markdown }
103
+ }
104
+
105
+ /**
106
+ * Split frontmatter into reserved keys and custom params.
107
+ * Exact match of content-collector line 642.
108
+ */
109
+ function splitParams(frontMatter) {
110
+ const {
111
+ type,
112
+ component,
113
+ preset,
114
+ input,
115
+ props,
116
+ fetch,
117
+ data,
118
+ id,
119
+ background,
120
+ theme,
121
+ ...params
122
+ } = frontMatter
123
+ return {
124
+ type: type || component || null,
125
+ preset: preset || null,
126
+ reserved: { data, id, background, theme, input },
127
+ params
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Guarantee item has flat content structure.
133
+ * Keep in sync with @uniweb/runtime/src/prepare-props.js
134
+ */
135
+ function guaranteeItemStructure(item) {
136
+ return {
137
+ title: item.title || '',
138
+ pretitle: item.pretitle || '',
139
+ subtitle: item.subtitle || '',
140
+ paragraphs: item.paragraphs || [],
141
+ links: item.links || [],
142
+ images: item.images || [],
143
+ lists: item.lists || [],
144
+ icons: item.icons || [],
145
+ videos: item.videos || [],
146
+ buttons: item.buttons || [],
147
+ data: item.data || {},
148
+ cards: item.cards || [],
149
+ documents: item.documents || [],
150
+ forms: item.forms || [],
151
+ quotes: item.quotes || [],
152
+ headings: item.headings || []
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Guarantee content structure exists.
158
+ * Keep in sync with @uniweb/runtime/src/prepare-props.js
159
+ */
160
+ function guaranteeContentStructure(parsedContent) {
161
+ const content = parsedContent || {}
162
+
163
+ return {
164
+ title: content.title || '',
165
+ pretitle: content.pretitle || '',
166
+ subtitle: content.subtitle || '',
167
+ alignment: content.alignment || null,
168
+ paragraphs: content.paragraphs || [],
169
+ links: content.links || [],
170
+ images: content.images || [],
171
+ lists: content.lists || [],
172
+ icons: content.icons || [],
173
+ videos: content.videos || [],
174
+ insets: content.insets || [],
175
+ buttons: content.buttons || [],
176
+ data: content.data || {},
177
+ cards: content.cards || [],
178
+ documents: content.documents || [],
179
+ forms: content.forms || [],
180
+ quotes: content.quotes || [],
181
+ headings: content.headings || [],
182
+ items: (content.items || []).map(guaranteeItemStructure),
183
+ sequence: content.sequence || [],
184
+ raw: content.raw
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Remove empty fields from content for clean output.
190
+ */
191
+ function removeEmptyFields(obj) {
192
+ const result = {}
193
+ for (const [key, value] of Object.entries(obj)) {
194
+ if (key === 'raw') continue // Always skip raw ProseMirror in clean output
195
+ if (value === null || value === undefined) continue
196
+ if (value === '') continue
197
+ if (Array.isArray(value) && value.length === 0) continue
198
+ if (
199
+ typeof value === 'object' &&
200
+ !Array.isArray(value) &&
201
+ Object.keys(value).length === 0
202
+ )
203
+ continue
204
+
205
+ // Recursively clean items
206
+ if (key === 'items' && Array.isArray(value)) {
207
+ result[key] = value.map((item) => removeEmptyFields(item))
208
+ } else {
209
+ result[key] = value
210
+ }
211
+ }
212
+ return result
213
+ }
214
+
215
+ /**
216
+ * Process a single markdown file and return its parsed shape.
217
+ */
218
+ function processFile(fileContent, fileName, deps, options) {
219
+ const { raw, full, sequence } = options
220
+ const { markdownToProseMirror, parseContent } = deps
221
+
222
+ const { frontMatter, markdown } = extractFrontmatter(fileContent)
223
+ const { type, preset, reserved, params } = splitParams(frontMatter)
224
+
225
+ // Parse markdown to ProseMirror — the document as stored, insets as written.
226
+ const stored = markdownToProseMirror(markdown)
227
+
228
+ // What the component receives: the insets lifted the way a Block lifts them —
229
+ // a placeholder in the content, the inset itself listed beside it. ⛔ This
230
+ // file carried its own copy of the site build's extractor until 2026-09-27,
231
+ // and it had drifted (top-level only, no inline insets, `description` for
232
+ // `title`); insets are lifted by core now, so this asks core.
233
+ const lifted = deps.liftInsets
234
+ ? deps.liftInsets(stored)
235
+ : { content: stored, leaves: [], containers: [] }
236
+ const doc = lifted.content
237
+ const insets = [
238
+ ...lifted.leaves,
239
+ ...lifted.containers.map(({ refId, type, params }) => ({ refId, type, params, container: true }))
240
+ ]
241
+
242
+ // Build result
243
+ const result = {}
244
+
245
+ if (type) result.type = type
246
+ if (preset) result.preset = preset
247
+
248
+ // Include non-empty reserved fields
249
+ for (const [key, value] of Object.entries(reserved)) {
250
+ if (value !== undefined && value !== null) result[key] = value
251
+ }
252
+
253
+ // Include params if any
254
+ if (Object.keys(params).length > 0) result.params = params
255
+
256
+ // Check if file is a child section
257
+ if (basename(fileName).startsWith('@')) {
258
+ result.child = true
259
+ }
260
+
261
+ if (raw) {
262
+ // Raw mode: the ProseMirror AST as stored — insets in it as the author wrote them.
263
+ result.prosemirror = stored
264
+ return result
265
+ }
266
+
267
+ // Parse to flat content shape
268
+ const parsed = parseContent(doc)
269
+
270
+ // Apply guarantees
271
+ let content = guaranteeContentStructure(parsed)
272
+
273
+ if (!full) {
274
+ content = removeEmptyFields(content)
275
+ } else {
276
+ // In full mode, still remove raw
277
+ delete content.raw
278
+ }
279
+
280
+ if (!sequence) {
281
+ delete content.sequence
282
+ }
283
+
284
+ result.content = content
285
+
286
+ if (insets.length > 0) result.insets = insets
287
+
288
+ // Grouping near-miss findings — shapes that usually mean the author got a
289
+ // different structure than they wanted (an entry absorbed into the
290
+ // headline, a hand-written eyebrow demoting the title). Observation only:
291
+ // the parse above is untouched.
292
+ const findings = deps.lintContent?.(doc) || []
293
+ if (findings.length > 0) result.findings = findings
294
+
295
+ return result
296
+ }
297
+
298
+ /**
299
+ * Natural sort comparator for filenames (handles numeric prefixes).
300
+ */
301
+ function naturalSort(a, b) {
302
+ return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })
303
+ }
304
+
305
+ /**
306
+ * Main inspect entry point.
307
+ */
308
+ export async function inspect(args) {
309
+ const flags = parseArgs(args)
310
+
311
+ if (flags.help || !flags.target) {
312
+ console.log(`
313
+ Usage: uniweb inspect <path> [options]
314
+
315
+ Inspect the parsed content shape of a markdown file or page folder.
316
+
317
+ Arguments:
318
+ <path> Path to a .md file or a page folder
319
+
320
+ Options:
321
+ --raw Show ProseMirror AST instead of flat content shape
322
+ --full Include empty fields (matches runtime guarantees)
323
+ --sequence Include sequence array (document-order elements)
324
+ -h, --help Show this help
325
+ `)
326
+ return
327
+ }
328
+
329
+ const target = resolve(process.cwd(), flags.target)
330
+ const deps = await loadDependencies()
331
+
332
+ let stat
333
+ try {
334
+ stat = statSync(target)
335
+ } catch {
336
+ console.error(`${colors.red}✗${colors.reset} Not found: ${flags.target}`)
337
+ process.exit(1)
338
+ }
339
+
340
+ const options = { raw: flags.raw, full: flags.full, sequence: flags.sequence }
341
+
342
+ if (stat.isFile()) {
343
+ if (extname(target) !== '.md') {
344
+ console.error(
345
+ `${colors.red}✗${colors.reset} Expected a .md file: ${flags.target}`
346
+ )
347
+ process.exit(1)
348
+ }
349
+ const content = readFileSync(target, 'utf8')
350
+ const result = processFile(content, basename(target), deps, options)
351
+ console.log(JSON.stringify(result, null, 2))
352
+ } else if (stat.isDirectory()) {
353
+ const files = readdirSync(target)
354
+ .filter(
355
+ (f) => extname(f) === '.md' && !f.startsWith('_') && f !== 'README.md'
356
+ )
357
+ .sort(naturalSort)
358
+
359
+ if (files.length === 0) {
360
+ console.error(
361
+ `${colors.yellow}No .md files found in: ${flags.target}${colors.reset}`
362
+ )
363
+ return
364
+ }
365
+
366
+ const results = files.map((file) => {
367
+ const content = readFileSync(resolve(target, file), 'utf8')
368
+ const result = processFile(content, file, deps, options)
369
+ result._file = file
370
+ return result
371
+ })
372
+
373
+ console.log(JSON.stringify(results, null, 2))
374
+ }
375
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * invite — RESERVED (not available on the new backend yet).
3
+ *
4
+ * The foundation client-invite flow ran against the legacy platform (the PHP
5
+ * backend + Cloudflare Worker) the CLI no longer talks to. Client onboarding is
6
+ * handled in the Uniweb app for now; a new-backend CLI equivalent isn't built.
7
+ * The command name is kept reserved so a future rebuild can claim it without a
8
+ * breaking change.
9
+ */
10
+
11
+ export async function invite() {
12
+ console.error(
13
+ "\x1b[31m✗\x1b[0m `uniweb invite` isn't available on the new backend yet."
14
+ )
15
+ console.error(
16
+ ' The legacy client-invite flow was retired with the PHP backend.'
17
+ )
18
+ console.error(
19
+ ' Invite clients to your foundation from the Uniweb app for now.'
20
+ )
21
+ process.exit(1)
22
+ }
23
+
24
+ export default invite
@@ -0,0 +1,96 @@
1
+ /**
2
+ * uniweb org — the orgs you belong to, on the backend you are logged in to.
3
+ *
4
+ * uniweb org list Your personal scope, and the orgs you're a member of.
5
+ * uniweb org create <handle> Create an org; you become a member.
6
+ *
7
+ * ⭐ You need no org to register a foundation: `@<your handle>` is your personal scope
8
+ * (2026-09-23). An org is for a scope — and sites — you share, and it needs a handle
9
+ * of its own: a backend refuses one named after an account, yours included.
10
+ *
11
+ * Auth: the session of the backend you are logged in to (`uniweb login`) / UNIWEB_TOKEN.
12
+ */
13
+
14
+ import { BackendClient } from '../backend/client.js'
15
+ import { validateHandle, bareHandle } from '../utils/registry-orgs.js'
16
+
17
+ const colors = {
18
+ reset: '\x1b[0m',
19
+ bright: '\x1b[1m',
20
+ dim: '\x1b[2m',
21
+ red: '\x1b[31m',
22
+ green: '\x1b[32m'
23
+ }
24
+ const error = (m) => console.error(`${colors.red}✗${colors.reset} ${m}`)
25
+ const success = (m) => console.log(`${colors.green}✓${colors.reset} ${m}`)
26
+
27
+ export async function org(args = []) {
28
+ const sub = args[0]
29
+
30
+ if (sub === 'list') {
31
+ const client = new BackendClient({ args, command: 'Listing orgs' })
32
+ const { account_handle: mine, orgs: all } = await client.fetchOrgs()
33
+ // An org named after you — made before 2026-09-23 — is your personal scope's `@`.
34
+ const orgs = all.filter((u) => u.handle !== mine)
35
+ if (mine) {
36
+ console.log(
37
+ `Your personal scope: ${colors.bright}@${mine}${colors.reset}${colors.dim} — register under it with no org${colors.reset}`
38
+ )
39
+ }
40
+ if (!orgs.length) {
41
+ console.log(
42
+ `You belong to no org. To share a scope and sites with others: uniweb org create <handle>`
43
+ )
44
+ return { exitCode: 0 }
45
+ }
46
+ console.log('Your orgs:')
47
+ for (const u of orgs) {
48
+ console.log(
49
+ ` ${colors.bright}@${u.handle}${colors.reset}${u.is_primary ? `${colors.dim} (primary)${colors.reset}` : ''}`
50
+ )
51
+ }
52
+ return { exitCode: 0 }
53
+ }
54
+
55
+ if (sub === 'create') {
56
+ const handle = bareHandle(args[1])
57
+ if (!handle) {
58
+ error('Usage: uniweb org create <handle>')
59
+ return { exitCode: 2 }
60
+ }
61
+ const invalid = validateHandle(handle)
62
+ if (invalid) {
63
+ error(invalid)
64
+ return { exitCode: 2 }
65
+ }
66
+ const client = new BackendClient({ args, command: 'Creating an org' })
67
+ try {
68
+ // Named after you, it is your personal scope already — and a backend refuses it.
69
+ const { account_handle: mine } = await client.fetchOrgs()
70
+ if (mine && handle === mine) {
71
+ error(
72
+ `@${handle} is your personal scope already — you can register under it with no org. An org needs a handle of its own.`
73
+ )
74
+ return { exitCode: 2 }
75
+ }
76
+ const org = await client.createOrg(handle)
77
+ success(
78
+ `Created ${colors.bright}@${org.handle}${colors.reset} — you're a member${org.is_primary ? ' (primary)' : ''}.`
79
+ )
80
+ console.log(
81
+ `${colors.dim}Register under it: uniweb register --scope @${org.handle}${colors.reset}`
82
+ )
83
+ return { exitCode: 0 }
84
+ } catch (err) {
85
+ error(err.message)
86
+ return { exitCode: 1 }
87
+ }
88
+ }
89
+
90
+ console.log('uniweb org <command>')
91
+ console.log(' list Your personal scope, and the orgs you belong to')
92
+ console.log(' create <handle> Create an org (you become a member)')
93
+ return { exitCode: sub ? 2 : 0 }
94
+ }
95
+
96
+ export default org