create-bridgething 0.12.0 → 0.14.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/dist/index.mjs CHANGED
@@ -1,12 +1,138 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
4
- import { dirname, extname, join, resolve } from "node:path";
4
+ import { basename, dirname, extname, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { v7 } from "uuid";
7
+ import { createInterface } from "node:readline/promises";
8
+ //#region src/prompt.ts
9
+ var Interactive = class {
10
+ rl = createInterface({
11
+ input: process.stdin,
12
+ output: process.stdout
13
+ });
14
+ async ask(question, fallback) {
15
+ return (await this.rl.question(`${question}${fallback ? ` (${fallback})` : ""}: `)).trim() || fallback;
16
+ }
17
+ async confirm(question) {
18
+ return /^y(es)?$/i.test((await this.rl.question(`${question} (y/N): `)).trim());
19
+ }
20
+ close() {
21
+ this.rl.close();
22
+ }
23
+ };
24
+ var Silent = class {
25
+ async ask(_question, fallback) {
26
+ return fallback;
27
+ }
28
+ async confirm() {
29
+ return false;
30
+ }
31
+ close() {}
32
+ };
33
+ function asker(interactive) {
34
+ return interactive && process.stdin.isTTY ? new Interactive() : new Silent();
35
+ }
36
+ function slugify(value) {
37
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
38
+ }
39
+ function isSlug(value) {
40
+ return /^[a-z0-9][a-z0-9-]*$/.test(value);
41
+ }
42
+ //#endregion
43
+ //#region src/template.ts
44
+ const RENAMES = {
45
+ _gitignore: ".gitignore",
46
+ _claude: ".claude",
47
+ _github: ".github",
48
+ _bunversion: ".bun-version",
49
+ "_prettierrc.js": ".prettierrc.js"
50
+ };
51
+ const BINARY_EXT = new Set([
52
+ ".ttf",
53
+ ".otf",
54
+ ".woff",
55
+ ".woff2",
56
+ ".png",
57
+ ".jpg",
58
+ ".jpeg",
59
+ ".gif",
60
+ ".webp",
61
+ ".avif",
62
+ ".ico",
63
+ ".wasm"
64
+ ]);
65
+ function substitute(raw, subs) {
66
+ return raw.replace(/__[A-Z0-9_]+__/g, (token) => subs[token] ?? token);
67
+ }
68
+ function copyTemplate(src, dest, subs) {
69
+ mkdirSync(dest, { recursive: true });
70
+ for (const entry of readdirSync(src)) {
71
+ const srcPath = join(src, entry);
72
+ const destPath = join(dest, RENAMES[entry] ?? entry);
73
+ if (statSync(srcPath).isDirectory()) copyTemplate(srcPath, destPath, subs);
74
+ else if (BINARY_EXT.has(extname(entry).toLowerCase())) copyFileSync(srcPath, destPath);
75
+ else writeFileSync(destPath, substitute(readFileSync(srcPath, "utf8"), subs));
76
+ }
77
+ }
78
+ function copyDir(src, dest) {
79
+ mkdirSync(dest, { recursive: true });
80
+ for (const entry of readdirSync(src)) {
81
+ const srcPath = join(src, entry);
82
+ const destPath = join(dest, entry);
83
+ if (statSync(srcPath).isDirectory()) copyDir(srcPath, destPath);
84
+ else copyFileSync(srcPath, destPath);
85
+ }
86
+ }
87
+ function move(from, to) {
88
+ if (!existsSync(from)) return;
89
+ rmSync(to, {
90
+ recursive: true,
91
+ force: true
92
+ });
93
+ mkdirSync(join(to, ".."), { recursive: true });
94
+ copyDir(from, to);
95
+ rmSync(from, {
96
+ recursive: true,
97
+ force: true
98
+ });
99
+ }
100
+ function linkOrCopy(target, linkPath, realPath) {
101
+ rmSync(linkPath, {
102
+ recursive: true,
103
+ force: true
104
+ });
105
+ mkdirSync(join(linkPath, ".."), { recursive: true });
106
+ try {
107
+ symlinkSync(target, linkPath, "dir");
108
+ } catch {
109
+ copyDir(realPath, linkPath);
110
+ }
111
+ }
112
+ function readJson(path) {
113
+ return JSON.parse(readFileSync(path, "utf8"));
114
+ }
115
+ function writeJson(path, value) {
116
+ mkdirSync(join(path, ".."), { recursive: true });
117
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
118
+ }
119
+ function patchJson(path, patch) {
120
+ writeJson(path, {
121
+ ...readJson(path),
122
+ ...patch
123
+ });
124
+ }
125
+ function sortKeys(record) {
126
+ const entries = Object.entries(record).sort(([a], [b]) => a.localeCompare(b));
127
+ return entries.length ? Object.fromEntries(entries) : void 0;
128
+ }
129
+ //#endregion
7
130
  //#region src/index.ts
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
- const TEMPLATE_DIR = resolve(__dirname, "..", "template");
131
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
132
+ const TEMPLATE_DIR = join(PACKAGE_ROOT, "template");
133
+ const SOURCE_TEMPLATE_DIR = join(PACKAGE_ROOT, "template-source");
134
+ const SKILL_IN_APP = join(".claude", "skills", "bridgething");
135
+ const SDK_VERSIONS = readJson(join(PACKAGE_ROOT, "template-versions.json"));
10
136
  const EXTENSION = {
11
137
  dir: "template-extension",
12
138
  manifest: {
@@ -23,7 +149,7 @@ const EXTENSION = {
23
149
  }
24
150
  }]
25
151
  },
26
- dependencies: { "@bridgething/extension": `^${JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf8")).version}` },
152
+ dependencies: { "@bridgething/extension": SDK_VERSIONS["@bridgething/extension"] },
27
153
  devDependencies: {
28
154
  deno: "2.9.6",
29
155
  esbuild: "^0.28.2"
@@ -51,67 +177,126 @@ const VARIANTS = {
51
177
  }
52
178
  };
53
179
  function parseArgs(argv) {
54
- const positional = [];
55
- let install = true;
56
- let git = true;
57
- let variant = "app";
58
- let extension = false;
59
- for (const arg of argv) if (arg === "--no-install") install = false;
60
- else if (arg === "--no-git") git = false;
61
- else if (arg === "--launcher") variant = "launcher";
62
- else if (arg === "--overlay") variant = "overlay";
63
- else if (arg === "--extension") extension = true;
64
- else if (arg === "--help" || arg === "-h") {
65
- printHelp();
66
- process.exit(0);
67
- } else if (arg.startsWith("-")) {
68
- console.error(`unknown flag: ${arg}`);
69
- printHelp();
70
- process.exit(1);
71
- } else positional.push(arg);
72
- if (positional.length !== 1) {
73
- printHelp();
74
- process.exit(1);
75
- }
76
- return {
77
- target: positional[0],
78
- install,
79
- git,
80
- variant,
81
- extension
180
+ const args = {
181
+ target: null,
182
+ install: true,
183
+ git: true,
184
+ interactive: true,
185
+ variant: "app",
186
+ extension: false,
187
+ sourceName: null,
188
+ sourceDescription: null,
189
+ repo: null
82
190
  };
191
+ const value = (at, flag) => argv[at] ?? die(`${flag} needs a value`);
192
+ for (let at = 0; at < argv.length; at++) {
193
+ const arg = argv[at];
194
+ if (arg === "--no-install") args.install = false;
195
+ else if (arg === "--no-git") args.git = false;
196
+ else if (arg === "--yes" || arg === "-y") args.interactive = false;
197
+ else if (arg === "--launcher") args.variant = "launcher";
198
+ else if (arg === "--overlay") args.variant = "overlay";
199
+ else if (arg === "--extension") args.extension = true;
200
+ else if (arg === "--source-name") args.sourceName = value(++at, "--source-name");
201
+ else if (arg === "--source-description") args.sourceDescription = value(++at, "--source-description");
202
+ else if (arg === "--repo") args.repo = value(++at, "--repo");
203
+ else if (arg === "--help" || arg === "-h") {
204
+ printHelp();
205
+ process.exit(0);
206
+ } else if (arg.startsWith("-")) die(`unknown flag: ${arg}`);
207
+ else if (args.target) die("name one target");
208
+ else args.target = arg;
209
+ }
210
+ return args;
83
211
  }
84
212
  function printHelp() {
85
- console.log(`Usage: create-bridgething <target-dir> [--launcher | --overlay] [--extension] [--no-install] [--no-git]
213
+ console.log(`Usage: create-bridgething [target] [options]
86
214
 
87
- Scaffold a new bridgething webapp at <target-dir>.
215
+ scaffold a bridgething app at [target].
88
216
 
89
217
  Variants:
90
- (default) A normal webapp.
91
- --launcher A home screen. Declares 'role: launcher' and can take the
92
- device's launcher slot, replacing the built-in hub.
93
- --overlay A system overlay. Ships an 'overlay.js' the daemon injects
94
- into every webapp, replacing the built-in one.
218
+ (default) a normal webapp
219
+ --launcher a home screen that can take the device's launcher slot, replacing the built-in hub
220
+ --overlay A system overlay that can be injected into every webapp, replacing the built-in one
95
221
 
96
- Add-ons (combine with any variant):
97
- --extension A native extension: a Deno process the desktop app runs
98
- alongside the webapp, with host access the manifest declares.
222
+ Add-ons:
223
+ --extension A native extension with a Deno process the desktop app runs alongside the webapp
99
224
 
100
225
  Options:
101
- --no-install Skip 'bun install' after copying.
102
- --no-git Skip 'git init' after copying.
226
+ --source-name <name> name the source (app store repo) instead of asking
227
+ --source-description <text> describe the source instead of asking
228
+ --repo <owner/repo> the github repo this source publishes from
229
+ -y, --yes take every default
230
+ --no-install skip 'bun install'
231
+ --no-git skip 'git init'
103
232
  `);
104
233
  }
105
- function patchJson(path, patch) {
106
- const parsed = JSON.parse(readFileSync(path, "utf8"));
107
- writeFileSync(path, `${JSON.stringify({
108
- ...parsed,
109
- ...patch
110
- }, null, 2)}\n`);
234
+ function die(message) {
235
+ console.error(`error: ${message}`);
236
+ process.exit(1);
237
+ }
238
+ function capture(cmd, args, cwd = process.cwd()) {
239
+ const result = spawnSync(cmd, args, {
240
+ encoding: "utf8",
241
+ cwd
242
+ });
243
+ return result.status === 0 ? result.stdout.trim() : null;
244
+ }
245
+ function run(cmd, args, cwd) {
246
+ return spawnSync(cmd, args, {
247
+ cwd,
248
+ stdio: "inherit"
249
+ }).status === 0;
250
+ }
251
+ function findSourceRoot(from) {
252
+ let dir = resolve(from);
253
+ for (;;) {
254
+ if (existsSync(join(dir, "source.json")) && existsSync(join(dir, "apps"))) return dir;
255
+ const parent = dirname(dir);
256
+ if (parent === dir) return null;
257
+ dir = parent;
258
+ }
259
+ }
260
+ function repoFromRemote(cwd) {
261
+ const remote = capture("git", [
262
+ "remote",
263
+ "get-url",
264
+ "origin"
265
+ ], cwd);
266
+ const match = remote ? /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(remote) : null;
267
+ return match ? `${match[1]}/${match[2]}` : null;
111
268
  }
112
- function patchPackage(target, patch) {
113
- const path = join(target, "package.json");
114
- const pkg = JSON.parse(readFileSync(path, "utf8"));
269
+ function sourceHomepage(root) {
270
+ const homepage = readJson(join(root, "source.json")).homepage ?? null;
271
+ return homepage && /^https:\/\/github\.com\/[^/]+\/[^/]+\/?$/.test(homepage) ? homepage.replace(/\/$/, "") : null;
272
+ }
273
+ function githubLogin() {
274
+ return capture("gh", [
275
+ "api",
276
+ "user",
277
+ "-q",
278
+ ".login"
279
+ ]) ?? capture("git", ["config", "github.user"]);
280
+ }
281
+ function splitRepo(repo) {
282
+ const match = /^([A-Za-z0-9][A-Za-z0-9-]*)\/([A-Za-z0-9._-]+)$/.exec(repo.replace(/\.git$/, ""));
283
+ if (!match) die(`"${repo}" is not owner/repo`);
284
+ return {
285
+ owner: match[1],
286
+ name: match[2]
287
+ };
288
+ }
289
+ function repoSubs(repo) {
290
+ return {
291
+ __REPO_OWNER__: repo.owner,
292
+ __REPO_OWNER_LOWER__: repo.owner.toLowerCase(),
293
+ __REPO_NAME__: repo.name,
294
+ __BASE_URL__: `https://${repo.owner.toLowerCase()}.github.io/${repo.name}`
295
+ };
296
+ }
297
+ function patchPackage(dir, patch) {
298
+ const path = join(dir, "package.json");
299
+ const pkg = readJson(path);
115
300
  patchJson(path, {
116
301
  scripts: {
117
302
  ...pkg.scripts,
@@ -127,147 +312,231 @@ function patchPackage(target, patch) {
127
312
  })
128
313
  });
129
314
  }
130
- function sortKeys(record) {
131
- return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
132
- }
133
315
  function overlayTemplate(target, dir, subs) {
134
- copyTemplate(resolve(__dirname, "..", dir), target, subs);
316
+ copyTemplate(join(PACKAGE_ROOT, dir), target, subs);
135
317
  const appendPath = join(target, "_claude_append.md");
136
318
  if (!existsSync(appendPath)) return;
137
319
  const claudePath = join(target, "CLAUDE.md");
138
- writeFileSync(claudePath, `${readFileSync(claudePath, "utf8").trimEnd()}\n\n${readFileSync(appendPath, "utf8")}`);
139
- rmSync(appendPath);
140
- }
141
- function applyVariant(target, variant, subs) {
142
- const spec = VARIANTS[variant];
143
- if (spec.dir) overlayTemplate(target, spec.dir, subs);
144
- if (spec.manifest) patchJson(join(target, "public", "manifest.json"), spec.manifest);
145
- if (spec.scripts) patchPackage(target, { scripts: spec.scripts });
146
- }
147
- function applyExtension(target, subs) {
148
- overlayTemplate(target, EXTENSION.dir, subs);
149
- patchJson(join(target, "public", "manifest.json"), EXTENSION.manifest);
150
- patchPackage(target, {
151
- dependencies: EXTENSION.dependencies,
152
- devDependencies: EXTENSION.devDependencies
153
- });
320
+ const addition = readFileSync(appendPath, "utf8");
321
+ const existing = existsSync(claudePath) ? `${readFileSync(claudePath, "utf8").trimEnd()}\n\n` : "";
322
+ writeFileSync(claudePath, `${existing}${existing ? addition : addition.replace(/^## /, "# ")}`);
323
+ rmSync(appendPath, { force: true });
154
324
  }
155
- const BINARY_EXT = new Set([
156
- ".ttf",
157
- ".otf",
158
- ".woff",
159
- ".woff2",
160
- ".png",
161
- ".jpg",
162
- ".jpeg",
163
- ".gif",
164
- ".webp",
165
- ".avif",
166
- ".ico",
167
- ".wasm"
168
- ]);
169
- function copyTemplate(src, dest, subs) {
170
- for (const entry of readdirSync(src)) {
171
- const srcPath = join(src, entry);
172
- const destPath = join(dest, entry === "_gitignore" ? ".gitignore" : entry === "_claude" ? ".claude" : entry);
173
- if (statSync(srcPath).isDirectory()) {
174
- mkdirSync(destPath, { recursive: true });
175
- copyTemplate(srcPath, destPath, subs);
176
- } else if (BINARY_EXT.has(extname(entry).toLowerCase())) copyFileSync(srcPath, destPath);
177
- else writeFileSync(destPath, readFileSync(srcPath, "utf8").replace(/__PROJECT_NAME__/g, subs.projectName).replace(/__WEBAPP_UUID__/g, subs.webappUuid));
325
+ function scaffoldApp(root, spec) {
326
+ const dir = join(root, "apps", spec.slug);
327
+ if (existsSync(dir) && readdirSync(dir).length) die(`apps/${spec.slug} already exists`);
328
+ const uuid = v7();
329
+ const subs = {
330
+ __PROJECT_NAME__: spec.slug,
331
+ __WEBAPP_UUID__: uuid,
332
+ __APP_SLUG__: spec.slug
333
+ };
334
+ copyTemplate(TEMPLATE_DIR, dir, subs);
335
+ patchPackage(dir, { dependencies: { "@bridgething/client": SDK_VERSIONS["@bridgething/client"] } });
336
+ const variant = VARIANTS[spec.variant];
337
+ if (variant.dir) overlayTemplate(dir, variant.dir, subs);
338
+ if (variant.manifest) patchJson(join(dir, "public", "manifest.json"), variant.manifest);
339
+ if (variant.scripts) patchPackage(dir, { scripts: variant.scripts });
340
+ if (spec.extension) {
341
+ overlayTemplate(dir, EXTENSION.dir, subs);
342
+ patchJson(join(dir, "public", "manifest.json"), EXTENSION.manifest);
343
+ patchPackage(dir, {
344
+ dependencies: EXTENSION.dependencies,
345
+ devDependencies: EXTENSION.devDependencies
346
+ });
178
347
  }
348
+ const tsconfigPath = join(dir, "tsconfig.json");
349
+ patchJson(tsconfigPath, { include: readJson(tsconfigPath).include.filter((entry) => existsSync(join(dir, entry))) });
350
+ writeJson(join(dir, "catalog.json"), {
351
+ author: spec.author,
352
+ homepage: spec.repoUrl,
353
+ source: spec.repoUrl,
354
+ icon: null,
355
+ screenshots: [],
356
+ min_libbridgething_version: SDK_VERSIONS.libbridgething
357
+ });
358
+ const manifest = readJson(join(dir, "public", "manifest.json"));
359
+ writeFileSync(join(dir, "CHANGELOG.md"), `# ${manifest.name}\n\n## ${manifest.version}\n\nFirst release.\n`);
360
+ return {
361
+ dir,
362
+ uuid,
363
+ shape: spec.extension ? `${variant.summary} + ${EXTENSION.summary}` : variant.summary
364
+ };
179
365
  }
180
- function copyDir(src, dest) {
181
- mkdirSync(dest, { recursive: true });
182
- for (const entry of readdirSync(src)) {
183
- const srcPath = join(src, entry);
184
- const destPath = join(dest, entry);
185
- if (statSync(srcPath).isDirectory()) copyDir(srcPath, destPath);
186
- else copyFileSync(srcPath, destPath);
366
+ function linkAgentGuides(root, appDir) {
367
+ const rootSkill = join(root, SKILL_IN_APP);
368
+ const appSkill = join(appDir, SKILL_IN_APP);
369
+ if (!existsSync(rootSkill)) move(appSkill, rootSkill);
370
+ linkOrCopy(relative(dirname(appSkill), rootSkill), appSkill, rootSkill);
371
+ for (const link of [join(root, "AGENTS.md"), join(appDir, "AGENTS.md")]) {
372
+ if (existsSync(link) || !existsSync(join(dirname(link), "CLAUDE.md"))) continue;
373
+ try {
374
+ symlinkSync("CLAUDE.md", link);
375
+ } catch {
376
+ writeFileSync(link, readFileSync(join(dirname(link), "CLAUDE.md"), "utf8"));
377
+ }
378
+ }
379
+ for (const dir of [root, appDir]) {
380
+ mkdirSync(join(dir, ".agents"), { recursive: true });
381
+ linkOrCopy(join("..", ".claude", "skills"), join(dir, ".agents", "skills"), join(dir, ".claude", "skills"));
187
382
  }
188
383
  }
189
- function linkAgentAliases(target) {
190
- try {
191
- symlinkSync("CLAUDE.md", join(target, "AGENTS.md"));
192
- } catch {
193
- copyFileSync(join(target, "CLAUDE.md"), join(target, "AGENTS.md"));
384
+ async function askSourceDetails(ask, args) {
385
+ const named = args.target && !isSlug(args.target) ? basename(resolve(args.target)) : args.target;
386
+ const name = args.sourceName ?? await ask.ask("Source name", named || "My Car Thing Apps");
387
+ const description = args.sourceDescription ?? await ask.ask("Source description", `Webapps by ${name}.`);
388
+ const suggested = `${githubLogin() ?? "your-github-username"}/${slugify(name)}`;
389
+ const repo = splitRepo(args.repo ?? await ask.ask("GitHub repo", suggested));
390
+ let app = args.target && isSlug(args.target) ? args.target : "";
391
+ while (!app) {
392
+ const answer = slugify(await ask.ask("First app", named ? slugify(named) : "weather"));
393
+ if (isSlug(answer)) app = answer;
194
394
  }
195
- mkdirSync(join(target, ".agents"), { recursive: true });
395
+ return {
396
+ name,
397
+ description,
398
+ repo,
399
+ app
400
+ };
401
+ }
402
+ async function createSource(args) {
403
+ const ask = asker(args.interactive);
404
+ let details;
196
405
  try {
197
- symlinkSync(join("..", ".claude", "skills"), join(target, ".agents", "skills"), "dir");
198
- } catch {
199
- copyDir(join(target, ".claude", "skills"), join(target, ".agents", "skills"));
406
+ details = await askSourceDetails(ask, args);
407
+ if (details.repo.owner === "your-github-username") die("pass --repo <owner/repo>, or answer the GitHub repo prompt; the catalog url is derived from it");
408
+ } finally {
409
+ ask.close();
200
410
  }
201
- }
202
- function run(cmd, args, cwd) {
203
- return spawnSync(cmd, args, {
204
- cwd,
205
- stdio: "inherit"
206
- }).status === 0;
207
- }
208
- function main() {
209
- const args = parseArgs(process.argv.slice(2));
210
- const target = resolve(process.cwd(), args.target);
211
- const projectName = args.target.replace(/^.*[\\/]/, "");
212
- if (existsSync(target)) {
213
- if (readdirSync(target).length > 0) {
214
- console.error(`error: ${target} exists and is not empty`);
215
- process.exit(1);
216
- }
217
- } else mkdirSync(target, { recursive: true });
218
- const webappUuid = v7();
411
+ const target = resolve(process.cwd(), args.target && !isSlug(args.target) ? args.target : details.repo.name);
412
+ if (existsSync(target) && readdirSync(target).length) die(`${target} exists and is not empty`);
219
413
  const subs = {
220
- projectName,
221
- webappUuid
414
+ ...repoSubs(details.repo),
415
+ __SOURCE_NAME__: details.name,
416
+ __SOURCE_DESCRIPTION__: details.description,
417
+ __APP_SLUG__: details.app
222
418
  };
223
- console.log(`scaffolding ${projectName} (${webappUuid}) in ${target}`);
224
- copyTemplate(TEMPLATE_DIR, target, subs);
225
- applyVariant(target, args.variant, subs);
226
- if (args.extension) applyExtension(target, subs);
227
- const shape = args.extension ? `${VARIANTS[args.variant].summary} + ${EXTENSION.summary}` : VARIANTS[args.variant].summary;
228
- console.log(` ✓ template copied (${shape})`);
229
- linkAgentAliases(target);
230
- console.log(" ✓ agent guides linked (CLAUDE.md, AGENTS.md, /bridgething skill)");
231
- if (args.git) if (run("git", ["init", "--quiet"], target)) console.log(" ✓ git initialized");
232
- else console.warn(" ! git init failed (skipping)");
419
+ console.log(`\nscaffolding ${details.name} in ${target}`);
420
+ copyTemplate(SOURCE_TEMPLATE_DIR, target, subs);
421
+ patchPackage(target, { devDependencies: { "@bridgething/source": SDK_VERSIONS["@bridgething/source"] } });
422
+ const repoUrl = `https://github.com/${details.repo.owner}/${details.repo.name}`;
423
+ const app = scaffoldApp(target, {
424
+ slug: details.app,
425
+ variant: args.variant,
426
+ extension: args.extension,
427
+ author: capture("git", ["config", "user.name"]) ?? details.repo.owner,
428
+ repoUrl
429
+ });
430
+ linkAgentGuides(target, app.dir);
431
+ console.log(` ✓ source and apps/${details.app} (${app.uuid}) created as a ${app.shape}`);
432
+ if (args.git && run("git", ["init", "--quiet"], target)) console.log(" ✓ git initialized");
233
433
  if (args.install) {
234
434
  console.log(" installing dependencies with bun...");
235
435
  if (!run("bun", ["install"], target)) console.warn(" ! bun install failed; install manually with `bun install`");
236
436
  else console.log(" ✓ dependencies installed");
237
437
  }
438
+ await offerRepoCreate(args, target, details.repo, details.description);
439
+ printSourceNextSteps(target, details, repoUrl);
440
+ }
441
+ async function offerRepoCreate(args, target, repo, description) {
442
+ if (!args.interactive || !args.git || !capture("gh", ["auth", "status"])) return;
443
+ const ask = asker(true);
444
+ try {
445
+ if (!await ask.confirm(`\nCreate github.com/${repo.owner}/${repo.name} and push now?`)) return;
446
+ } finally {
447
+ ask.close();
448
+ }
449
+ run("git", ["add", "--all"], target);
450
+ run("git", [
451
+ "commit",
452
+ "--quiet",
453
+ "-m",
454
+ "scaffold the app source"
455
+ ], target);
456
+ const created = run("gh", [
457
+ "repo",
458
+ "create",
459
+ `${repo.owner}/${repo.name}`,
460
+ "--public",
461
+ "--source",
462
+ ".",
463
+ "--push",
464
+ "-d",
465
+ description
466
+ ], target);
467
+ console.log(created ? " ✓ repo created and pushed" : " ! gh repo create failed; push it yourself");
468
+ }
469
+ function printSourceNextSteps(target, details, repoUrl) {
470
+ const base = `https://${details.repo.owner.toLowerCase()}.github.io/${details.repo.name}`;
238
471
  console.log(`
239
- Done! Next steps:
472
+ Done. ${target}
240
473
 
241
- cd ${args.target}
242
- ${args.install ? "" : " bun install\n"} bun run dev # local dev server (http://localhost:5173/) against the connected Car Thing
243
- bun run dev:device # the same server on the Car Thing's own screen, hot reload included
474
+ cd ${basename(target)}
475
+ bun run dev # local dev server against the connected Car Thing
476
+ bun run dev:device # dev server on the Car Thing's own screen
477
+ bun run check # check that your app and source is valid
244
478
 
245
- Open this folder with your coding agent (Claude Code, Codex, opencode, ...).
246
- It reads CLAUDE.md / AGENTS.md and the /bridgething skill in .claude/skills
247
- (mirrored at .agents/skills), which goes deep on the client API, running and
248
- driving the app, and installing and sharing it.
479
+ Fill in the description in apps/${details.app}/public/manifest.json.
249
480
 
250
- bun run build # production bundle into dist/
251
- bun run push <addr> # build + install onto a connected Car Thing (default bridgething.local)
252
- bun run share # build first, then zip dist/ to hand to friends
253
- bun run update # bring the connected Car Thing to the latest bridgething release
254
- ${args.extension ? `
255
- 'extension/main.ts' is the desktop-side half: a Deno process the bridgething
256
- desktop app runs alongside this webapp, with the host access 'manifest.json'
257
- declares. 'bun run dev' and 'bun run dev:device' also run it, rebuilt on every
258
- save and wired to the connected Car Thing, so both halves iterate together.
259
- 'bun run build' bundles it to dist/extension/desktop.mjs.
260
- ` : ""}${args.variant === "app" ? "" : `
261
- 'bun run push' also claims the device's ${args.variant} slot, so this is live the
262
- moment it lands. 'bun run push --release' hands the slot back to the built-in
263
- ${args.variant === "launcher" ? "hub" : "overlay"}, which is how you recover if a build of this wedges the screen.
264
- The companion phone app can do the same thing.
265
- `}
266
- The starter App connects to the daemon through daemonUrl() in src/daemon.ts: the
267
- local daemon on the device, and the dev server's proxy to the Car Thing over USB
268
- under bun run dev (SUPERBIRD_HOST picks another device).
481
+ Push to ${repoUrl}, then in Settings > Pages set the branch to gh-pages, folder / (root).
482
+ Your catalog ends up at at ${base}/catalog.v1.json, which you can submit to the bridgething store.
483
+
484
+ bun run new <slug> add another app to this source
485
+ bun run bump ${details.app} [version]
269
486
  `);
270
487
  }
271
- main();
488
+ async function addApp(root, args) {
489
+ const ask = asker(args.interactive);
490
+ let slug = args.target ?? "";
491
+ try {
492
+ while (!isSlug(slug)) {
493
+ if (slug) console.error(`"${slug}" is not a usable slug; use lowercase letters, digits and dashes`);
494
+ slug = slugify(await ask.ask("App slug", "weather"));
495
+ if (!args.interactive) break;
496
+ }
497
+ } finally {
498
+ ask.close();
499
+ }
500
+ if (!isSlug(slug)) die("name the app: create-bridgething <slug>");
501
+ const repo = repoFromRemote(root);
502
+ const repoUrl = repo ? `https://github.com/${repo}` : sourceHomepage(root);
503
+ if (args.extension && !repoUrl) console.warn("warning: this source has no github origin, so catalog.json \"source\" is null. An app that ships an\n extension must have its repo url before it can be published.");
504
+ const app = scaffoldApp(root, {
505
+ slug,
506
+ variant: args.variant,
507
+ extension: args.extension,
508
+ author: capture("git", ["config", "user.name"], root) ?? "Your Name",
509
+ repoUrl
510
+ });
511
+ linkAgentGuides(root, app.dir);
512
+ console.log(`apps/${slug} (${app.uuid}) created as a ${app.shape}`);
513
+ if (args.install && !run("bun", ["install"], root)) console.warn("! bun install failed. install manually with `bun install`");
514
+ console.log(`
515
+ Next:
516
+
517
+ fill in the description in apps/${slug}/public/manifest.json, and check the author
518
+ and repo url in apps/${slug}/catalog.json
519
+
520
+ bun run dev ${slug} develop against a connected Car Thing
521
+ bun run check ensure your app and source are valid
522
+ bun run bump ${slug} patch
523
+
524
+ Push to main and the publish workflow puts it in your catalog.`);
525
+ }
526
+ async function main() {
527
+ const args = parseArgs(process.argv.slice(2));
528
+ const root = findSourceRoot(process.cwd());
529
+ if (root) await addApp(root, args);
530
+ else await createSource(args);
531
+ }
532
+ try {
533
+ await main();
534
+ } catch (err) {
535
+ if (err.name === "AbortError") {
536
+ console.log("\ncancelled; nothing was written.");
537
+ process.exit(130);
538
+ }
539
+ throw err;
540
+ }
272
541
  //#endregion
273
542
  export {};