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.
- package/README.md +16 -12
- package/dist/index.mjs +476 -142
- package/package.json +20 -5
- package/template/_claude/skills/bridgething/SKILL.md +12 -17
- package/template/_claude/skills/bridgething/reference/develop.md +86 -66
- package/template/_claude/skills/bridgething/reference/extension.md +183 -0
- package/template/_claude/skills/bridgething/reference/sdk.md +86 -105
- package/template/_claude/skills/bridgething/reference/ship.md +19 -38
- package/template/_gitignore +1 -0
- package/template/package.json +6 -5
- package/template/scripts/bridgething.ts +1086 -0
- package/template/scripts/push.ts +212 -193
- package/template/settings/main.tsx +14 -13
- package/template/src/App.tsx +3 -6
- package/template/src/daemon.ts +14 -0
- package/template/tsconfig.json +1 -1
- package/template/vite.config.ts +5 -3
- package/template/vite.settings.config.ts +3 -3
- package/template-extension/_claude_append.md +23 -0
- package/template-extension/extension/main.ts +20 -0
- package/template-launcher/_claude_append.md +14 -30
- package/template-launcher/src/App.tsx +2 -5
- package/template-overlay/_claude_append.md +31 -45
- 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 -95
- 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
76
|
-
|
|
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
|
|
83
|
-
|
|
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
|
|
118
|
-
if (statSync(srcPath).isDirectory())
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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(
|
|
107
|
+
symlinkSync(target, linkPath, "dir");
|
|
137
108
|
} catch {
|
|
138
|
-
|
|
109
|
+
copyDir(realPath, linkPath);
|
|
139
110
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
}
|
|
163
|
-
const
|
|
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
|
-
|
|
166
|
-
|
|
414
|
+
...repoSubs(details.repo),
|
|
415
|
+
__SOURCE_NAME__: details.name,
|
|
416
|
+
__SOURCE_DESCRIPTION__: details.description,
|
|
417
|
+
__APP_SLUG__: details.app
|
|
167
418
|
};
|
|
168
|
-
console.log(
|
|
169
|
-
copyTemplate(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
|
193
|
-
bun run
|
|
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
|
-
|
|
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 {};
|