create-bridgething 0.12.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.
- package/dist/index.mjs +439 -170
- package/package.json +13 -4
- package/template/_claude/skills/bridgething/reference/develop.md +5 -4
- package/template/_claude/skills/bridgething/reference/sdk.md +13 -0
- package/template/package.json +1 -1
- package/template/settings/main.tsx +14 -13
- package/template/src/App.tsx +1 -1
- package/template/tsconfig.json +1 -1
- package/template/vite.settings.config.ts +3 -3
- package/template-overlay/vite.overlay.config.ts +1 -1
- package/template-source/CLAUDE.md +104 -0
- package/template-source/README.md +57 -0
- package/template-source/_bunversion +1 -0
- package/template-source/_github/workflows/ci.yml +28 -0
- package/template-source/_github/workflows/publish.yml +33 -0
- package/template-source/_gitignore +7 -0
- package/template-source/_prettierrc.js +10 -0
- package/template-source/apps/.gitkeep +0 -0
- package/template-source/package.json +27 -0
- package/template-source/source.json +8 -0
- package/template-versions.json +6 -0
- package/template/CLAUDE.md +0 -71
- package/template/README.md +0 -57
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
|
|
9
|
-
const TEMPLATE_DIR =
|
|
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":
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
213
|
+
console.log(`Usage: create-bridgething [target] [options]
|
|
86
214
|
|
|
87
|
-
|
|
215
|
+
scaffold a bridgething app at [target].
|
|
88
216
|
|
|
89
217
|
Variants:
|
|
90
|
-
(default)
|
|
91
|
-
--launcher
|
|
92
|
-
|
|
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
|
|
97
|
-
--extension A native extension
|
|
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
|
-
--
|
|
102
|
-
--
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
113
|
-
const
|
|
114
|
-
|
|
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(
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
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
|
-
|
|
156
|
-
"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
414
|
+
...repoSubs(details.repo),
|
|
415
|
+
__SOURCE_NAME__: details.name,
|
|
416
|
+
__SOURCE_DESCRIPTION__: details.description,
|
|
417
|
+
__APP_SLUG__: details.app
|
|
222
418
|
};
|
|
223
|
-
console.log(
|
|
224
|
-
copyTemplate(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
472
|
+
Done. ${target}
|
|
240
473
|
|
|
241
|
-
cd ${
|
|
242
|
-
|
|
243
|
-
bun run dev:device #
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
bun run
|
|
254
|
-
${
|
|
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
|
-
|
|
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 {};
|