@donezone/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/done +0 -0
- package/dist/index.js +484 -27
- package/package.json +5 -4
package/dist/done
ADDED
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
2
6
|
import { Command } from "commander";
|
|
7
|
+
const DEFAULT_TEMPLATE_REPO = "https://github.com/mccallofthewild/done-template.git";
|
|
3
8
|
const program = new Command();
|
|
4
9
|
program.name("done").description("Done developer toolkit (spec-first stub)").version("0.0.0-spec");
|
|
5
10
|
/* NOTE: All of these commands should be expected to be run from a standalone binary, not from the context of this repository / workspace. */
|
|
@@ -8,18 +13,14 @@ program
|
|
|
8
13
|
.description("Bundle Done contracts defined in done.json")
|
|
9
14
|
.option("-j, --contracts <path>", "Path to done.json")
|
|
10
15
|
.option("-n, --name <name...>", "Filter contract names")
|
|
11
|
-
.action(() => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
If cwd is a contract workspace, build all contract packages in workspace.
|
|
21
|
-
|
|
22
|
-
*/
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
await handleBuild(options);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
23
24
|
});
|
|
24
25
|
program
|
|
25
26
|
.command("dev")
|
|
@@ -28,20 +29,19 @@ program
|
|
|
28
29
|
.action(() => notImplemented("dev"));
|
|
29
30
|
program
|
|
30
31
|
.command("init")
|
|
31
|
-
.
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*/
|
|
32
|
+
.argument("[name]", "Project or contract name")
|
|
33
|
+
.option("-f, --force", "Overwrite existing files if they already exist")
|
|
34
|
+
.option("--template <repoOrPath>", "Template git repo or local folder (defaults to official Done template)")
|
|
35
|
+
.option("--no-install", "Skip Bun install after creating a brand new workspace")
|
|
36
|
+
.description("Set up a new Done workspace or add a contract to an existing workspace")
|
|
37
|
+
.action(async (rawName, options) => {
|
|
38
|
+
try {
|
|
39
|
+
await handleInit(rawName, options);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
45
|
});
|
|
46
46
|
program
|
|
47
47
|
.command("deploy")
|
|
@@ -52,8 +52,465 @@ program
|
|
|
52
52
|
program
|
|
53
53
|
.command("test")
|
|
54
54
|
.description("Run unit tests for contracts");
|
|
55
|
-
program.parseAsync().catch((error) =>
|
|
55
|
+
program.parseAsync().catch((error) => {
|
|
56
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
56
59
|
function notImplemented(label) {
|
|
57
60
|
console.error(`The command segment '${label}' is not implemented yet.\nSee packages/done-cli/SPEC.md for the planned behaviour.`);
|
|
58
61
|
process.exit(1);
|
|
59
62
|
}
|
|
63
|
+
async function handleInit(rawName, options) {
|
|
64
|
+
const cwd = process.cwd();
|
|
65
|
+
const workspace = await detectWorkspace(cwd);
|
|
66
|
+
if (workspace) {
|
|
67
|
+
if (!rawName) {
|
|
68
|
+
throw new Error("Contract name is required when running init inside an existing Done workspace.");
|
|
69
|
+
}
|
|
70
|
+
await scaffoldContract(workspace, rawName, options);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!rawName) {
|
|
74
|
+
throw new Error("Project name is required when creating a brand new Done workspace.");
|
|
75
|
+
}
|
|
76
|
+
await scaffoldWorkspace(rawName, options);
|
|
77
|
+
}
|
|
78
|
+
async function handleBuild(options) {
|
|
79
|
+
const manifestInfo = await resolveManifest(options.contracts);
|
|
80
|
+
const contracts = manifestInfo.manifest.contracts ?? [];
|
|
81
|
+
if (contracts.length === 0) {
|
|
82
|
+
throw new Error(`No contracts defined in ${path.relative(process.cwd(), manifestInfo.manifestPath) || manifestInfo.manifestPath}`);
|
|
83
|
+
}
|
|
84
|
+
const filters = (options.name ?? [])
|
|
85
|
+
.flatMap((pattern) => pattern.split(","))
|
|
86
|
+
.map((pattern) => pattern.trim())
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
const matchers = filters.map(createGlobMatcher);
|
|
89
|
+
const selected = matchers.length === 0
|
|
90
|
+
? contracts
|
|
91
|
+
: contracts.filter((contract) => matchesContract(contract, manifestInfo.manifestDir, matchers));
|
|
92
|
+
if (selected.length === 0) {
|
|
93
|
+
throw new Error(`No contracts matched filters: ${filters.join(", ")}`);
|
|
94
|
+
}
|
|
95
|
+
console.log(`Building ${selected.length} contract${selected.length === 1 ? "" : "s"} from ${path.relative(process.cwd(), manifestInfo.manifestPath) || manifestInfo.manifestPath}`);
|
|
96
|
+
for (const contract of selected) {
|
|
97
|
+
if (!contract.entry) {
|
|
98
|
+
throw new Error(`Contract '${contract.name}' is missing an 'entry' path in ${manifestInfo.manifestPath}`);
|
|
99
|
+
}
|
|
100
|
+
if (!contract.outFile) {
|
|
101
|
+
throw new Error(`Contract '${contract.name}' is missing an 'outFile' path in ${manifestInfo.manifestPath}`);
|
|
102
|
+
}
|
|
103
|
+
const entryPath = path.resolve(manifestInfo.manifestDir, contract.entry);
|
|
104
|
+
const outFilePath = path.resolve(manifestInfo.manifestDir, contract.outFile);
|
|
105
|
+
await ensureEntryExists(entryPath, contract.name);
|
|
106
|
+
await fs.mkdir(path.dirname(outFilePath), { recursive: true });
|
|
107
|
+
const relEntry = path.relative(process.cwd(), entryPath) || entryPath;
|
|
108
|
+
const relOutFile = path.relative(process.cwd(), outFilePath) || outFilePath;
|
|
109
|
+
console.log(`- bundling ${contract.name} (${relEntry})`);
|
|
110
|
+
const startedAt = Date.now();
|
|
111
|
+
try {
|
|
112
|
+
await runBunBuild(entryPath, outFilePath);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error(`[error] failed to build ${contract.name}`);
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
const duration = Date.now() - startedAt;
|
|
119
|
+
console.log(` [done] ${contract.name} -> ${relOutFile} (${formatDuration(duration)})`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function detectWorkspace(root) {
|
|
123
|
+
const manifestPath = path.join(root, "done.json");
|
|
124
|
+
const configPath = path.join(root, "done.config.json");
|
|
125
|
+
if (!existsSync(manifestPath) || !existsSync(configPath)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const [manifestRaw, configRaw] = await Promise.all([
|
|
129
|
+
fs.readFile(manifestPath, "utf8"),
|
|
130
|
+
fs.readFile(configPath, "utf8"),
|
|
131
|
+
]);
|
|
132
|
+
const manifest = JSON.parse(manifestRaw);
|
|
133
|
+
const config = JSON.parse(configRaw);
|
|
134
|
+
return { root, manifestPath, configPath, manifest, config };
|
|
135
|
+
}
|
|
136
|
+
async function resolveManifest(contractsPathOverride) {
|
|
137
|
+
if (contractsPathOverride) {
|
|
138
|
+
const manifestPath = path.resolve(process.cwd(), contractsPathOverride);
|
|
139
|
+
const manifest = await readManifest(manifestPath);
|
|
140
|
+
return { manifestPath, manifestDir: path.dirname(manifestPath), manifest };
|
|
141
|
+
}
|
|
142
|
+
const workspace = await findWorkspace(process.cwd());
|
|
143
|
+
if (!workspace) {
|
|
144
|
+
throw new Error("Unable to find done.json. Run inside a Done workspace or pass --contracts <path>.");
|
|
145
|
+
}
|
|
146
|
+
return { manifestPath: workspace.manifestPath, manifestDir: path.dirname(workspace.manifestPath), manifest: workspace.manifest };
|
|
147
|
+
}
|
|
148
|
+
async function findWorkspace(startDir) {
|
|
149
|
+
let current = path.resolve(startDir);
|
|
150
|
+
while (true) {
|
|
151
|
+
const workspace = await detectWorkspace(current);
|
|
152
|
+
if (workspace) {
|
|
153
|
+
return workspace;
|
|
154
|
+
}
|
|
155
|
+
const parent = path.dirname(current);
|
|
156
|
+
if (parent === current) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
current = parent;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function readManifest(manifestPath) {
|
|
163
|
+
let raw;
|
|
164
|
+
try {
|
|
165
|
+
raw = await fs.readFile(manifestPath, "utf8");
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error.code === "ENOENT") {
|
|
169
|
+
throw new Error(`Could not find ${manifestPath}. Pass a valid --contracts path or run from a Done workspace.`);
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(raw);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
throw new Error(`Failed to parse ${manifestPath}: ${error.message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function scaffoldWorkspace(rawName, options) {
|
|
181
|
+
const slug = toSlug(rawName);
|
|
182
|
+
if (!slug) {
|
|
183
|
+
throw new Error(`Could not derive a valid directory name from '${rawName}'.`);
|
|
184
|
+
}
|
|
185
|
+
const templateRef = options.template ?? DEFAULT_TEMPLATE_REPO;
|
|
186
|
+
const cwd = process.cwd();
|
|
187
|
+
const targetDir = path.resolve(cwd, slug);
|
|
188
|
+
await prepareTargetDir(targetDir, options.force === true);
|
|
189
|
+
const localTemplatePath = await resolveLocalTemplate(templateRef);
|
|
190
|
+
if (localTemplatePath) {
|
|
191
|
+
await fs.cp(localTemplatePath, targetDir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
await runCommand("git", ["clone", "--depth", "1", templateRef, targetDir]);
|
|
195
|
+
}
|
|
196
|
+
await removeGitFolder(targetDir);
|
|
197
|
+
await updatePackageJson(path.join(targetDir, "package.json"), (pkg) => {
|
|
198
|
+
pkg.name = slug;
|
|
199
|
+
});
|
|
200
|
+
await updatePackageJson(path.join(targetDir, "frontend", "package.json"), (pkg) => {
|
|
201
|
+
pkg.name = `${slug}-frontend`;
|
|
202
|
+
});
|
|
203
|
+
if (options.install !== false) {
|
|
204
|
+
await runCommand("bun", ["install"], { cwd: targetDir });
|
|
205
|
+
}
|
|
206
|
+
console.log(`Done workspace created at ${path.relative(cwd, targetDir) || "."}.`);
|
|
207
|
+
console.log("Next steps:");
|
|
208
|
+
console.log(` cd ${path.relative(cwd, targetDir) || slug}`);
|
|
209
|
+
if (options.install === false) {
|
|
210
|
+
console.log(" bun install");
|
|
211
|
+
}
|
|
212
|
+
console.log(" bunx done dev");
|
|
213
|
+
}
|
|
214
|
+
async function scaffoldContract(workspace, rawName, options) {
|
|
215
|
+
const slug = toSlug(rawName);
|
|
216
|
+
if (!slug) {
|
|
217
|
+
throw new Error(`Could not derive a valid contract directory name from '${rawName}'.`);
|
|
218
|
+
}
|
|
219
|
+
const contractName = toContractName(slug);
|
|
220
|
+
const contractsDir = resolveContractsDir(workspace.root, workspace.config.contractsDir);
|
|
221
|
+
const contractDir = path.join(contractsDir, slug);
|
|
222
|
+
await ensureDirAvailable(contractDir, options.force === true);
|
|
223
|
+
await fs.mkdir(path.join(contractDir, "src"), { recursive: true });
|
|
224
|
+
const pkgJsonPath = path.join(contractDir, "package.json");
|
|
225
|
+
const pkgJson = {
|
|
226
|
+
name: slug,
|
|
227
|
+
version: "0.1.0",
|
|
228
|
+
private: true,
|
|
229
|
+
type: "module",
|
|
230
|
+
scripts: {
|
|
231
|
+
typecheck: "tsc -p tsconfig.json --noEmit",
|
|
232
|
+
},
|
|
233
|
+
dependencies: {
|
|
234
|
+
"@donezone/contract-types": "^0.1.0",
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
await writeJson(pkgJsonPath, pkgJson);
|
|
238
|
+
const rootTsconfig = path.join(workspace.root, "tsconfig.json");
|
|
239
|
+
const tsconfig = {
|
|
240
|
+
compilerOptions: {
|
|
241
|
+
module: "ESNext",
|
|
242
|
+
moduleResolution: "bundler",
|
|
243
|
+
target: "ES2022",
|
|
244
|
+
isolatedModules: true,
|
|
245
|
+
noEmit: true,
|
|
246
|
+
},
|
|
247
|
+
include: ["src/**/*.ts"],
|
|
248
|
+
};
|
|
249
|
+
if (existsSync(rootTsconfig)) {
|
|
250
|
+
tsconfig.extends = normalizeJsonPath(path.relative(contractDir, rootTsconfig));
|
|
251
|
+
}
|
|
252
|
+
await writeJson(path.join(contractDir, "tsconfig.json"), tsconfig);
|
|
253
|
+
const storeKey = `${slug}-greeting`;
|
|
254
|
+
const defaultGreeting = `Hello from ${contractName}`;
|
|
255
|
+
const contractSource = `const Greeting = Done.item<string>("${storeKey}");
|
|
256
|
+
|
|
257
|
+
interface SetGreetingBody {
|
|
258
|
+
greeting: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export default Done.serve()
|
|
262
|
+
.instantiate(async () => {
|
|
263
|
+
const existing = await Greeting.load();
|
|
264
|
+
if (!existing) {
|
|
265
|
+
await Greeting.save("${defaultGreeting}");
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
.query("/greeting", async () => {
|
|
269
|
+
const greeting = await Greeting.load();
|
|
270
|
+
return { greeting: greeting ?? "${defaultGreeting}" };
|
|
271
|
+
})
|
|
272
|
+
.transaction(
|
|
273
|
+
"/greeting",
|
|
274
|
+
async ({ body }) => {
|
|
275
|
+
const payload = (body ?? {}) as Partial<SetGreetingBody>;
|
|
276
|
+
const next = payload.greeting?.trim();
|
|
277
|
+
|
|
278
|
+
if (!next) {
|
|
279
|
+
throw new Error("greeting text required");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await Greeting.save(next);
|
|
283
|
+
return { greeting: next };
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
body: {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {
|
|
289
|
+
greeting: { type: "string", minLength: 1, maxLength: 280 },
|
|
290
|
+
},
|
|
291
|
+
required: ["greeting"],
|
|
292
|
+
additionalProperties: false,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
`;
|
|
297
|
+
await fs.writeFile(path.join(contractDir, "src", "index.ts"), contractSource, "utf8");
|
|
298
|
+
workspace.manifest.contracts ??= [];
|
|
299
|
+
const contractDirRel = normalizeJsonPath(path.relative(workspace.root, contractDir));
|
|
300
|
+
const entry = ensureDotSlash(`${contractDirRel}/src/index.ts`);
|
|
301
|
+
const outFile = ensureDotSlash(`${contractDirRel}/dist/${slug}.contract.js`);
|
|
302
|
+
const watch = [ensureDotSlash(`${contractDirRel}/src/**/*.ts`)];
|
|
303
|
+
const newEntry = {
|
|
304
|
+
name: contractName,
|
|
305
|
+
entry,
|
|
306
|
+
outFile,
|
|
307
|
+
watch,
|
|
308
|
+
instantiate: { admin: null },
|
|
309
|
+
};
|
|
310
|
+
const existingIndex = workspace.manifest.contracts.findIndex((contract) => contract.name === contractName);
|
|
311
|
+
if (existingIndex >= 0 && options.force !== true) {
|
|
312
|
+
throw new Error(`Contract '${contractName}' already exists in done.json. Use --force to replace it.`);
|
|
313
|
+
}
|
|
314
|
+
if (existingIndex >= 0) {
|
|
315
|
+
workspace.manifest.contracts[existingIndex] = newEntry;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
workspace.manifest.contracts.push(newEntry);
|
|
319
|
+
}
|
|
320
|
+
await writeJson(workspace.manifestPath, workspace.manifest);
|
|
321
|
+
console.log(`Created Done contract '${contractName}' in ${path.relative(workspace.root, contractDir)}`);
|
|
322
|
+
console.log("Next steps:");
|
|
323
|
+
console.log(" bunx done build --name", contractName);
|
|
324
|
+
console.log(" bunx done dev");
|
|
325
|
+
}
|
|
326
|
+
async function resolveLocalTemplate(reference) {
|
|
327
|
+
const potentialPath = path.isAbsolute(reference) ? reference : path.resolve(process.cwd(), reference);
|
|
328
|
+
try {
|
|
329
|
+
const stats = await fs.stat(potentialPath);
|
|
330
|
+
if (stats.isDirectory()) {
|
|
331
|
+
return potentialPath;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch { }
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
async function prepareTargetDir(targetDir, force) {
|
|
338
|
+
if (!existsSync(targetDir)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!force) {
|
|
342
|
+
throw new Error(`Directory '${path.relative(process.cwd(), targetDir) || targetDir}' already exists. Use --force to overwrite.`);
|
|
343
|
+
}
|
|
344
|
+
await fs.rm(targetDir, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
async function ensureDirAvailable(targetDir, force) {
|
|
347
|
+
if (!existsSync(targetDir)) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (!force) {
|
|
351
|
+
throw new Error(`Directory '${path.relative(process.cwd(), targetDir) || targetDir}' already exists. Use --force to overwrite.`);
|
|
352
|
+
}
|
|
353
|
+
await fs.rm(targetDir, { recursive: true, force: true });
|
|
354
|
+
}
|
|
355
|
+
async function removeGitFolder(targetDir) {
|
|
356
|
+
await fs.rm(path.join(targetDir, ".git"), { recursive: true, force: true });
|
|
357
|
+
}
|
|
358
|
+
function resolveContractsDir(root, configured) {
|
|
359
|
+
if (!configured) {
|
|
360
|
+
return path.join(root, "contracts");
|
|
361
|
+
}
|
|
362
|
+
if (path.isAbsolute(configured)) {
|
|
363
|
+
return configured;
|
|
364
|
+
}
|
|
365
|
+
return path.resolve(root, configured);
|
|
366
|
+
}
|
|
367
|
+
async function updatePackageJson(file, mutator) {
|
|
368
|
+
try {
|
|
369
|
+
const current = JSON.parse(await fs.readFile(file, "utf8"));
|
|
370
|
+
mutator(current);
|
|
371
|
+
await writeJson(file, current);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (error.code === "ENOENT") {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function ensureDotSlash(value) {
|
|
381
|
+
return value.startsWith("./") || value.startsWith("../") ? value : `./${value}`;
|
|
382
|
+
}
|
|
383
|
+
function normalizeJsonPath(relativePath) {
|
|
384
|
+
const normalized = relativePath.split(path.sep).join("/");
|
|
385
|
+
return normalized.startsWith("./") || normalized.startsWith("../") ? normalized : `./${normalized}`;
|
|
386
|
+
}
|
|
387
|
+
function matchesContract(contract, manifestDir, matchers) {
|
|
388
|
+
const candidates = [];
|
|
389
|
+
if (contract.name) {
|
|
390
|
+
candidates.push(normalizeMatchValue(contract.name));
|
|
391
|
+
}
|
|
392
|
+
if (contract.entry) {
|
|
393
|
+
candidates.push(normalizeMatchValue(contract.entry));
|
|
394
|
+
candidates.push(normalizeMatchValue(path.resolve(manifestDir, contract.entry)));
|
|
395
|
+
}
|
|
396
|
+
if (contract.outFile) {
|
|
397
|
+
candidates.push(normalizeMatchValue(contract.outFile));
|
|
398
|
+
candidates.push(normalizeMatchValue(path.resolve(manifestDir, contract.outFile)));
|
|
399
|
+
}
|
|
400
|
+
return matchers.some((matcher) => candidates.some((candidate) => matcher(candidate)));
|
|
401
|
+
}
|
|
402
|
+
function createGlobMatcher(pattern) {
|
|
403
|
+
const normalized = normalizeMatchValue(pattern);
|
|
404
|
+
const hasWildcards = /[\*\?]/.test(normalized);
|
|
405
|
+
if (!hasWildcards) {
|
|
406
|
+
return (candidate) => normalizeMatchValue(candidate) === normalized;
|
|
407
|
+
}
|
|
408
|
+
const escaped = escapeRegExp(normalized)
|
|
409
|
+
.replace(/\\\*/g, ".*")
|
|
410
|
+
.replace(/\\\?/g, ".");
|
|
411
|
+
const regex = new RegExp(`^${escaped}$`);
|
|
412
|
+
return (candidate) => regex.test(normalizeMatchValue(candidate));
|
|
413
|
+
}
|
|
414
|
+
function normalizeMatchValue(value) {
|
|
415
|
+
if (!value) {
|
|
416
|
+
return "";
|
|
417
|
+
}
|
|
418
|
+
let normalized = value.replace(/\\/g, "/");
|
|
419
|
+
if (normalized.startsWith("./")) {
|
|
420
|
+
normalized = normalized.slice(2);
|
|
421
|
+
}
|
|
422
|
+
return normalized;
|
|
423
|
+
}
|
|
424
|
+
function escapeRegExp(value) {
|
|
425
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
426
|
+
}
|
|
427
|
+
async function ensureEntryExists(entryPath, contractName) {
|
|
428
|
+
try {
|
|
429
|
+
const stats = await fs.stat(entryPath);
|
|
430
|
+
if (!stats.isFile()) {
|
|
431
|
+
const relPath = path.relative(process.cwd(), entryPath) || entryPath;
|
|
432
|
+
throw new Error(`Entry path for contract '${contractName}' is not a file (${relPath})`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
if (error.code === "ENOENT") {
|
|
437
|
+
const rel = path.relative(process.cwd(), entryPath) || entryPath;
|
|
438
|
+
throw new Error(`Entry file for contract '${contractName}' not found (${rel})`);
|
|
439
|
+
}
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async function runBunBuild(entryPath, outFilePath) {
|
|
444
|
+
const bun = getBunRuntime();
|
|
445
|
+
const result = await bun.build({
|
|
446
|
+
entrypoints: [entryPath],
|
|
447
|
+
target: "bun",
|
|
448
|
+
format: "esm",
|
|
449
|
+
splitting: false,
|
|
450
|
+
sourcemap: "none",
|
|
451
|
+
external: ["bun:*", "node:*"],
|
|
452
|
+
define: {
|
|
453
|
+
"process.env.NODE_ENV": '"production"',
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
if (!result.success) {
|
|
457
|
+
const logs = (result.logs ?? []).map((log) => log?.message ?? String(log));
|
|
458
|
+
throw new Error(logs.join("\n") || "Bun.build failed");
|
|
459
|
+
}
|
|
460
|
+
const [output] = result.outputs;
|
|
461
|
+
if (!output) {
|
|
462
|
+
throw new Error("Bun.build did not emit an output file");
|
|
463
|
+
}
|
|
464
|
+
const bundledSource = await output.text();
|
|
465
|
+
await fs.writeFile(outFilePath, bundledSource);
|
|
466
|
+
}
|
|
467
|
+
function getBunRuntime() {
|
|
468
|
+
const bun = globalThis.Bun;
|
|
469
|
+
if (!bun || typeof bun.build !== "function") {
|
|
470
|
+
throw new Error("Bun runtime unavailable. Rebuild the done CLI using Bun to embed the runtime.");
|
|
471
|
+
}
|
|
472
|
+
return bun;
|
|
473
|
+
}
|
|
474
|
+
function formatDuration(ms) {
|
|
475
|
+
if (ms < 1000) {
|
|
476
|
+
return `${ms}ms`;
|
|
477
|
+
}
|
|
478
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
479
|
+
}
|
|
480
|
+
function toSlug(input) {
|
|
481
|
+
return input
|
|
482
|
+
.trim()
|
|
483
|
+
.toLowerCase()
|
|
484
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
485
|
+
.replace(/^-+/, "")
|
|
486
|
+
.replace(/-+$/, "");
|
|
487
|
+
}
|
|
488
|
+
function toContractName(slug) {
|
|
489
|
+
const parts = slug.split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
490
|
+
if (parts.length === 0) {
|
|
491
|
+
return "contract";
|
|
492
|
+
}
|
|
493
|
+
const [first, ...rest] = parts;
|
|
494
|
+
return first + rest.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
495
|
+
}
|
|
496
|
+
async function writeJson(file, data) {
|
|
497
|
+
const json = JSON.stringify(data, null, 2);
|
|
498
|
+
await fs.writeFile(file, `${json}\n`, "utf8");
|
|
499
|
+
}
|
|
500
|
+
async function runCommand(command, args, options = {}) {
|
|
501
|
+
await new Promise((resolve, reject) => {
|
|
502
|
+
const child = spawn(command, args, {
|
|
503
|
+
cwd: options.cwd,
|
|
504
|
+
stdio: "inherit",
|
|
505
|
+
});
|
|
506
|
+
child.on("close", (code) => {
|
|
507
|
+
if (code === 0) {
|
|
508
|
+
resolve();
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}`));
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
child.on("error", (error) => reject(error));
|
|
515
|
+
});
|
|
516
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@donezone/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"done": "dist/
|
|
7
|
+
"done": "dist/done"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "tsc -p tsconfig.json",
|
|
14
|
-
"clean": "rimraf dist"
|
|
13
|
+
"build": "tsc -p tsconfig.json && bun build ./src/index.ts --compile --outfile dist/done",
|
|
14
|
+
"clean": "rimraf dist",
|
|
15
|
+
"test": "bun test"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
18
|
"@donezone/core": "^0.1.0",
|