create-bridgething 0.11.0 → 0.12.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.
Files changed (37) hide show
  1. package/README.md +16 -12
  2. package/dist/index.mjs +476 -142
  3. package/package.json +20 -5
  4. package/template/_claude/skills/bridgething/SKILL.md +12 -17
  5. package/template/_claude/skills/bridgething/reference/develop.md +86 -66
  6. package/template/_claude/skills/bridgething/reference/extension.md +183 -0
  7. package/template/_claude/skills/bridgething/reference/sdk.md +86 -105
  8. package/template/_claude/skills/bridgething/reference/ship.md +19 -38
  9. package/template/_gitignore +1 -0
  10. package/template/package.json +6 -5
  11. package/template/scripts/bridgething.ts +1086 -0
  12. package/template/scripts/push.ts +212 -193
  13. package/template/settings/main.tsx +14 -13
  14. package/template/src/App.tsx +3 -6
  15. package/template/src/daemon.ts +14 -0
  16. package/template/tsconfig.json +1 -1
  17. package/template/vite.config.ts +5 -3
  18. package/template/vite.settings.config.ts +3 -3
  19. package/template-extension/_claude_append.md +23 -0
  20. package/template-extension/extension/main.ts +20 -0
  21. package/template-launcher/_claude_append.md +14 -30
  22. package/template-launcher/src/App.tsx +2 -5
  23. package/template-overlay/_claude_append.md +31 -45
  24. package/template-overlay/vite.overlay.config.ts +1 -1
  25. package/template-source/CLAUDE.md +104 -0
  26. package/template-source/README.md +57 -0
  27. package/template-source/_bunversion +1 -0
  28. package/template-source/_github/workflows/ci.yml +28 -0
  29. package/template-source/_github/workflows/publish.yml +33 -0
  30. package/template-source/_gitignore +7 -0
  31. package/template-source/_prettierrc.js +10 -0
  32. package/template-source/apps/.gitkeep +0 -0
  33. package/template-source/package.json +27 -0
  34. package/template-source/source.json +8 -0
  35. package/template-versions.json +6 -0
  36. package/template/CLAUDE.md +0 -95
  37. package/template/README.md +0 -70
package/dist/index.mjs CHANGED
@@ -1,102 +1,53 @@
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
- //#region src/index.ts
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
- const TEMPLATE_DIR = resolve(__dirname, "..", "template");
10
- const VARIANTS = {
11
- app: { summary: "webapp" },
12
- launcher: {
13
- dir: "template-launcher",
14
- manifest: {
15
- role: "launcher",
16
- description: "A custom home screen."
17
- },
18
- summary: "launcher (a replacement home screen)"
19
- },
20
- overlay: {
21
- dir: "template-overlay",
22
- manifest: {
23
- overlay: "overlay.js",
24
- description: "A custom system overlay."
25
- },
26
- scripts: { build: "vite build && vite build -c vite.settings.config.ts && vite build -c vite.overlay.config.ts" },
27
- summary: "overlay (system UI drawn over every webapp)"
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();
28
22
  }
29
23
  };
30
- function parseArgs(argv) {
31
- const positional = [];
32
- let install = true;
33
- let git = true;
34
- let variant = "app";
35
- for (const arg of argv) if (arg === "--no-install") install = false;
36
- else if (arg === "--no-git") git = false;
37
- else if (arg === "--launcher") variant = "launcher";
38
- else if (arg === "--overlay") variant = "overlay";
39
- else if (arg === "--help" || arg === "-h") {
40
- printHelp();
41
- process.exit(0);
42
- } else if (arg.startsWith("-")) {
43
- console.error(`unknown flag: ${arg}`);
44
- printHelp();
45
- process.exit(1);
46
- } else positional.push(arg);
47
- if (positional.length !== 1) {
48
- printHelp();
49
- process.exit(1);
24
+ var Silent = class {
25
+ async ask(_question, fallback) {
26
+ return fallback;
50
27
  }
51
- return {
52
- target: positional[0],
53
- install,
54
- git,
55
- variant
56
- };
57
- }
58
- function printHelp() {
59
- console.log(`Usage: create-bridgething <target-dir> [--launcher | --overlay] [--no-install] [--no-git]
60
-
61
- Scaffold a new bridgething webapp at <target-dir>.
62
-
63
- Variants:
64
- (default) A normal webapp.
65
- --launcher A home screen. Declares 'role: launcher' and can take the
66
- device's launcher slot, replacing the built-in hub.
67
- --overlay A system overlay. Ships an 'overlay.js' the daemon injects
68
- into every webapp, replacing the built-in one.
69
-
70
- Options:
71
- --no-install Skip 'bun install' after copying.
72
- --no-git Skip 'git init' after copying.
73
- `);
28
+ async confirm() {
29
+ return false;
30
+ }
31
+ close() {}
32
+ };
33
+ function asker(interactive) {
34
+ return interactive && process.stdin.isTTY ? new Interactive() : new Silent();
74
35
  }
75
- function patchJson(path, patch) {
76
- const parsed = JSON.parse(readFileSync(path, "utf8"));
77
- writeFileSync(path, `${JSON.stringify({
78
- ...parsed,
79
- ...patch
80
- }, null, 2)}\n`);
36
+ function slugify(value) {
37
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
81
38
  }
82
- function applyVariant(target, variant, subs) {
83
- const spec = VARIANTS[variant];
84
- if (spec.dir) copyTemplate(resolve(__dirname, "..", spec.dir), target, subs);
85
- const appendPath = join(target, "_claude_append.md");
86
- if (existsSync(appendPath)) {
87
- const claudePath = join(target, "CLAUDE.md");
88
- writeFileSync(claudePath, readFileSync(claudePath, "utf8") + readFileSync(appendPath, "utf8"));
89
- rmSync(appendPath);
90
- }
91
- if (spec.manifest) patchJson(join(target, "public", "manifest.json"), spec.manifest);
92
- if (spec.scripts) {
93
- const pkgPath = join(target, "package.json");
94
- patchJson(pkgPath, { scripts: {
95
- ...JSON.parse(readFileSync(pkgPath, "utf8")).scripts,
96
- ...spec.scripts
97
- } });
98
- }
39
+ function isSlug(value) {
40
+ return /^[a-z0-9][a-z0-9-]*$/.test(value);
99
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
+ };
100
51
  const BINARY_EXT = new Set([
101
52
  ".ttf",
102
53
  ".otf",
@@ -111,15 +62,17 @@ const BINARY_EXT = new Set([
111
62
  ".ico",
112
63
  ".wasm"
113
64
  ]);
65
+ function substitute(raw, subs) {
66
+ return raw.replace(/__[A-Z0-9_]+__/g, (token) => subs[token] ?? token);
67
+ }
114
68
  function copyTemplate(src, dest, subs) {
69
+ mkdirSync(dest, { recursive: true });
115
70
  for (const entry of readdirSync(src)) {
116
71
  const srcPath = join(src, entry);
117
- const destPath = join(dest, entry === "_gitignore" ? ".gitignore" : entry === "_claude" ? ".claude" : entry);
118
- if (statSync(srcPath).isDirectory()) {
119
- mkdirSync(destPath, { recursive: true });
120
- copyTemplate(srcPath, destPath, subs);
121
- } else if (BINARY_EXT.has(extname(entry).toLowerCase())) copyFileSync(srcPath, destPath);
122
- else writeFileSync(destPath, readFileSync(srcPath, "utf8").replace(/__PROJECT_NAME__/g, subs.projectName).replace(/__WEBAPP_UUID__/g, subs.webappUuid));
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));
123
76
  }
124
77
  }
125
78
  function copyDir(src, dest) {
@@ -131,18 +84,163 @@ function copyDir(src, dest) {
131
84
  else copyFileSync(srcPath, destPath);
132
85
  }
133
86
  }
134
- function linkAgentAliases(target) {
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 });
135
106
  try {
136
- symlinkSync("CLAUDE.md", join(target, "AGENTS.md"));
107
+ symlinkSync(target, linkPath, "dir");
137
108
  } catch {
138
- copyFileSync(join(target, "CLAUDE.md"), join(target, "AGENTS.md"));
109
+ copyDir(realPath, linkPath);
139
110
  }
140
- mkdirSync(join(target, ".agents"), { recursive: true });
141
- try {
142
- symlinkSync(join("..", ".claude", "skills"), join(target, ".agents", "skills"), "dir");
143
- } catch {
144
- copyDir(join(target, ".claude", "skills"), join(target, ".agents", "skills"));
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
130
+ //#region src/index.ts
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"));
136
+ const EXTENSION = {
137
+ dir: "template-extension",
138
+ manifest: {
139
+ extension: {
140
+ entry: "extension/desktop.mjs",
141
+ permissions: ["all"],
142
+ api: 1
143
+ },
144
+ config: [{
145
+ type: "string",
146
+ data: {
147
+ key: "greeting",
148
+ label: "Greeting the extension sends on connect"
149
+ }
150
+ }]
151
+ },
152
+ dependencies: { "@bridgething/extension": SDK_VERSIONS["@bridgething/extension"] },
153
+ devDependencies: {
154
+ deno: "2.9.6",
155
+ esbuild: "^0.28.2"
156
+ },
157
+ summary: "native extension (a desktop-side Deno process)"
158
+ };
159
+ const VARIANTS = {
160
+ app: { summary: "webapp" },
161
+ launcher: {
162
+ dir: "template-launcher",
163
+ manifest: {
164
+ role: "launcher",
165
+ description: "A custom home screen."
166
+ },
167
+ summary: "launcher (a replacement home screen)"
168
+ },
169
+ overlay: {
170
+ dir: "template-overlay",
171
+ manifest: {
172
+ overlay: "overlay.js",
173
+ description: "A custom system overlay."
174
+ },
175
+ scripts: { build: "vite build && vite build -c vite.settings.config.ts && vite build -c vite.overlay.config.ts" },
176
+ summary: "overlay (system UI drawn over every webapp)"
177
+ }
178
+ };
179
+ function parseArgs(argv) {
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
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;
145
209
  }
210
+ return args;
211
+ }
212
+ function printHelp() {
213
+ console.log(`Usage: create-bridgething [target] [options]
214
+
215
+ scaffold a bridgething app at [target].
216
+
217
+ Variants:
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
221
+
222
+ Add-ons:
223
+ --extension A native extension with a Deno process the desktop app runs alongside the webapp
224
+
225
+ Options:
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'
232
+ `);
233
+ }
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;
146
244
  }
147
245
  function run(cmd, args, cwd) {
148
246
  return spawnSync(cmd, args, {
@@ -150,59 +248,295 @@ function run(cmd, args, cwd) {
150
248
  stdio: "inherit"
151
249
  }).status === 0;
152
250
  }
153
- function main() {
154
- const args = parseArgs(process.argv.slice(2));
155
- const target = resolve(process.cwd(), args.target);
156
- const projectName = args.target.replace(/^.*[\\/]/, "");
157
- if (existsSync(target)) {
158
- if (readdirSync(target).length > 0) {
159
- console.error(`error: ${target} exists and is not empty`);
160
- process.exit(1);
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;
268
+ }
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);
300
+ patchJson(path, {
301
+ scripts: {
302
+ ...pkg.scripts,
303
+ ...patch.scripts
304
+ },
305
+ dependencies: sortKeys({
306
+ ...pkg.dependencies,
307
+ ...patch.dependencies
308
+ }),
309
+ devDependencies: sortKeys({
310
+ ...pkg.devDependencies,
311
+ ...patch.devDependencies
312
+ })
313
+ });
314
+ }
315
+ function overlayTemplate(target, dir, subs) {
316
+ copyTemplate(join(PACKAGE_ROOT, dir), target, subs);
317
+ const appendPath = join(target, "_claude_append.md");
318
+ if (!existsSync(appendPath)) return;
319
+ const claudePath = join(target, "CLAUDE.md");
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 });
324
+ }
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
+ });
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
+ };
365
+ }
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"));
161
377
  }
162
- } else mkdirSync(target, { recursive: true });
163
- const webappUuid = v7();
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"));
382
+ }
383
+ }
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;
394
+ }
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;
405
+ try {
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();
410
+ }
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`);
164
413
  const subs = {
165
- projectName,
166
- webappUuid
414
+ ...repoSubs(details.repo),
415
+ __SOURCE_NAME__: details.name,
416
+ __SOURCE_DESCRIPTION__: details.description,
417
+ __APP_SLUG__: details.app
167
418
  };
168
- console.log(`scaffolding ${projectName} (${webappUuid}) in ${target}`);
169
- copyTemplate(TEMPLATE_DIR, target, subs);
170
- applyVariant(target, args.variant, subs);
171
- console.log(` ✓ template copied (${VARIANTS[args.variant].summary})`);
172
- linkAgentAliases(target);
173
- console.log(" ✓ agent guides linked (CLAUDE.md, AGENTS.md, /bridgething skill)");
174
- if (args.git) if (run("git", ["init", "--quiet"], target)) console.log(" ✓ git initialized");
175
- 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");
176
433
  if (args.install) {
177
434
  console.log(" installing dependencies with bun...");
178
435
  if (!run("bun", ["install"], target)) console.warn(" ! bun install failed; install manually with `bun install`");
179
436
  else console.log(" ✓ dependencies installed");
180
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}`;
181
471
  console.log(`
182
- Done! Next steps:
472
+ Done. ${target}
473
+
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
183
478
 
184
- cd ${args.target}
185
- ${args.install ? "" : " bun install\n"} bun run dev # local dev server (http://localhost:5173/)
479
+ Fill in the description in apps/${details.app}/public/manifest.json.
186
480
 
187
- Open this folder with your coding agent (Claude Code, Codex, opencode, ...).
188
- It reads CLAUDE.md / AGENTS.md and the /bridgething skill in .claude/skills
189
- (mirrored at .agents/skills), which goes deep on the client API, running and
190
- driving the app, and installing and sharing it.
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.
191
483
 
192
- bun run build # production bundle into dist/
193
- bun run push <addr> # build + install onto a connected Car Thing (default bridgething.local)
194
- bun run share # build first, then zip dist/ to hand to friends
195
- bun run update # bring the connected Car Thing to the latest bridgething release
196
- ${args.variant === "app" ? "" : `
197
- 'bun run push' also claims the device's ${args.variant} slot, so this is live the
198
- moment it lands. 'bun run push --release' hands the slot back to the built-in
199
- ${args.variant === "launcher" ? "hub" : "overlay"}, which is how you recover if a build of this wedges the screen.
200
- The companion phone app can do the same thing.
201
- `}
202
- The starter App connects to ws://127.0.0.1:8891/ on the device, and to
203
- ws://<device-ip>:8891/ in dev when you set VITE_BRIDGETHING_URL.
484
+ bun run new <slug> add another app to this source
485
+ bun run bump ${details.app} [version]
204
486
  `);
205
487
  }
206
- 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
+ }
207
541
  //#endregion
208
542
  export {};