@donezone/cli 0.1.15 → 0.1.27

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 (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +912 -420
  3. package/package.json +10 -3
package/dist/index.js CHANGED
@@ -3,256 +3,267 @@ import { spawn } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
4
  import * as fs from "node:fs/promises";
5
5
  import path from "node:path";
6
+ import chokidar from "chokidar";
7
+ import pc from "picocolors";
6
8
  import { Command } from "commander";
9
+ import { DoneBackendClient } from "@donezone/client";
10
+ import { DoneLocalChain, deployJsContract } from "@donezone/local-chain";
11
+ import { createDoneHttpLocalServer } from "@donezone/http-local";
12
+ import { createDoneEventsLocalServer } from "@donezone/events-local";
7
13
  const DEFAULT_TEMPLATE_REPO = "https://github.com/mccallofthewild/done-template.git";
8
- const program = new Command();
14
+ const program = new Command;
9
15
  program.name("done").description("Done developer toolkit (spec-first stub)").version("0.0.0-spec");
10
- /* NOTE: All of these commands should be expected to be run from a standalone binary, not from the context of this repository / workspace. */
11
- program
12
- .command("build")
13
- .description("Bundle Done contracts defined in done.json")
14
- .option("-j, --contracts <path>", "Path to done.json")
15
- .option("-n, --name <name...>", "Filter contract names")
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
- }
16
+ program.command("build").description("Bundle Done contracts defined in done.json").option("-j, --contracts <path>", "Path to done.json").option("-n, --name <name...>", "Filter contract names").action(async (options) => {
17
+ try {
18
+ await handleBuild(options);
19
+ } catch (error) {
20
+ console.error(error instanceof Error ? error.message : String(error));
21
+ process.exit(1);
22
+ }
24
23
  });
25
- program
26
- .command("dev")
27
- .description("Launch frontend dev server plus Done HTTP Local backend, watch sources, rebuild & redeploy contracts automatically")
28
- .option("-b, --backend", "Run backend only")
29
- .action(() => notImplemented("dev"));
30
- program
31
- .command("init")
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
- }
24
+ program.command("dev").description("Boot Done local chain + HTTP + events stack with live publish_code reloads and optional frontend dev server").option("--backend", "Start backend services (done-local-chain, done-http-local, done-events-local)").option("--frontend", "Start the frontend dev server").action(async (options) => {
25
+ try {
26
+ await handleDev(options);
27
+ } catch (error) {
28
+ console.error(error instanceof Error ? error.message : String(error));
29
+ process.exit(1);
30
+ }
45
31
  });
46
- program
47
- .command("deploy")
48
- .description("Deploy latest bundles to the configured Done workspace")
49
- .option("-e, --env <profile>", "Environment/profile name")
50
- .option("-f, --fresh", "Force rebuild before deploy")
51
- .action(() => notImplemented("deploy"));
52
- program
53
- .command("test")
54
- .description("Run unit tests for contracts");
55
- program.parseAsync().catch((error) => {
32
+ program.command("init").argument("[name]", "Project or contract name").option("-f, --force", "Overwrite existing files if they already exist").option("--template <repoOrPath>", "Template git repo or local folder (defaults to official Done template)").option("--no-install", "Skip Bun install after creating a brand new workspace").description("Set up a new Done workspace or add a contract to an existing workspace").action(async (rawName, options) => {
33
+ try {
34
+ await handleInit(rawName, options);
35
+ } catch (error) {
56
36
  console.error(error instanceof Error ? error.message : String(error));
57
37
  process.exit(1);
38
+ }
58
39
  });
59
- function notImplemented(label) {
60
- console.error(`The command segment '${label}' is not implemented yet.\nSee packages/done-cli/SPEC.md for the planned behaviour.`);
40
+ program.command("deploy").description("Deploy latest bundles to the configured Done workspace").option("-e, --env <profile>", "Environment/profile name").option("-f, --fresh", "Force rebuild before deploy").action(() => notImplemented("deploy"));
41
+ program.command("test").description("Run unit tests for contracts").action(() => notImplemented("test"));
42
+ program.command("publish").description("Publish a new JS bundle to an existing Done contract via Done HTTP").requiredOption("-c, --contract <address>", "Target Done contract address").requiredOption("-s, --script <path>", "Path to bundled JS module").option("-m, --msg <jsonOrPath>", "Inline JSON payload or path to migrate message").option("--http <url>", "Done HTTP base URL (defaults to DONE_HTTP or http://localhost:3000)").action(async (options) => {
43
+ try {
44
+ await handlePublish(options);
45
+ } catch (error) {
46
+ console.error(error instanceof Error ? error.message : String(error));
61
47
  process.exit(1);
48
+ }
49
+ });
50
+ program.parseAsync().catch((error) => {
51
+ console.error(error instanceof Error ? error.message : String(error));
52
+ process.exit(1);
53
+ });
54
+ function notImplemented(label) {
55
+ console.error(`The command segment '${label}' is not implemented yet.\nSee packages/done-cli/SPEC.md for the planned behaviour.`);
56
+ process.exit(1);
62
57
  }
63
58
  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
- }
59
+ const cwd = process.cwd();
60
+ const workspace = await detectWorkspace(cwd);
61
+ if (workspace) {
73
62
  if (!rawName) {
74
- throw new Error("Project name is required when creating a brand new Done workspace.");
63
+ throw new Error("Contract name is required when running init inside an existing Done workspace.");
75
64
  }
76
- await scaffoldWorkspace(rawName, options);
65
+ await scaffoldContract(workspace, rawName, options);
66
+ return;
67
+ }
68
+ if (!rawName) {
69
+ throw new Error("Project name is required when creating a brand new Done workspace.");
70
+ }
71
+ await scaffoldWorkspace(rawName, options);
77
72
  }
78
73
  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)})`);
74
+ const manifestInfo = await resolveManifest(options.contracts);
75
+ const contracts = manifestInfo.manifest.contracts ?? [];
76
+ if (contracts.length === 0) {
77
+ throw new Error(`No contracts defined in ${path.relative(process.cwd(), manifestInfo.manifestPath) || manifestInfo.manifestPath}`);
78
+ }
79
+ const filters = (options.name ?? []).flatMap((pattern) => pattern.split(",")).map((pattern) => pattern.trim()).filter(Boolean);
80
+ const matchers = filters.map(createGlobMatcher);
81
+ const selected = matchers.length === 0 ? contracts : contracts.filter((contract) => matchesContract(contract, manifestInfo.manifestDir, matchers));
82
+ if (selected.length === 0) {
83
+ throw new Error(`No contracts matched filters: ${filters.join(", ")}`);
84
+ }
85
+ console.log(`Building ${selected.length} contract${selected.length === 1 ? "" : "s"} from ${path.relative(process.cwd(), manifestInfo.manifestPath) || manifestInfo.manifestPath}`);
86
+ for (const contract of selected) {
87
+ if (!contract.entry) {
88
+ throw new Error(`Contract '${contract.name}' is missing an 'entry' path in ${manifestInfo.manifestPath}`);
89
+ }
90
+ if (!contract.outFile) {
91
+ throw new Error(`Contract '${contract.name}' is missing an 'outFile' path in ${manifestInfo.manifestPath}`);
120
92
  }
93
+ const entryPath = path.resolve(manifestInfo.manifestDir, contract.entry);
94
+ const outFilePath = path.resolve(manifestInfo.manifestDir, contract.outFile);
95
+ await ensureEntryExists(entryPath, contract.name);
96
+ await fs.mkdir(path.dirname(outFilePath), { recursive: true });
97
+ const relEntry = path.relative(process.cwd(), entryPath) || entryPath;
98
+ const relOutFile = path.relative(process.cwd(), outFilePath) || outFilePath;
99
+ console.log(`- bundling ${contract.name} (${relEntry})`);
100
+ const startedAt = Date.now();
101
+ try {
102
+ await runBunBuild(entryPath, outFilePath);
103
+ } catch (error) {
104
+ console.error(`[error] failed to build ${contract.name}`);
105
+ throw error;
106
+ }
107
+ const duration = Date.now() - startedAt;
108
+ console.log(` [done] ${contract.name} -> ${relOutFile} (${formatDuration(duration)})`);
109
+ }
110
+ }
111
+ async function handlePublish(options) {
112
+ const scriptPath = path.resolve(process.cwd(), options.script);
113
+ await ensureFileExists(scriptPath, "script");
114
+ const script = await fs.readFile(scriptPath, "utf8");
115
+ const msg = options.msg ? await loadMsgPayload(options.msg) : undefined;
116
+ const baseUrl = resolveDoneHttpBase(options.http);
117
+ const backend = new DoneBackendClient({ baseUrl });
118
+ console.log(`Publishing ${path.basename(scriptPath)} to ${options.contract} via ${baseUrl}`);
119
+ const result = await backend.publishCode({
120
+ contract: options.contract,
121
+ script,
122
+ msg
123
+ });
124
+ console.log(` [tx] ${result.transactionHash ?? "(pending)"}`);
125
+ console.log(` [height] ${result.height}`);
126
+ if (typeof result.gasUsed === "number") {
127
+ console.log(` [gas] ${result.gasUsed}`);
128
+ }
129
+ if (result.data !== undefined) {
130
+ console.log(` [data] ${JSON.stringify(result.data)}`);
131
+ }
121
132
  }
122
133
  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 };
134
+ const configPath = path.join(root, "done.config.json");
135
+ if (!existsSync(configPath)) {
136
+ return null;
137
+ }
138
+ const configRaw = await fs.readFile(configPath, "utf8");
139
+ const config = JSON.parse(configRaw);
140
+ const configuredManifestPath = config.contractsFile ? path.isAbsolute(config.contractsFile) ? config.contractsFile : path.resolve(root, config.contractsFile) : path.join(root, "done.json");
141
+ const fallbackManifestPath = path.join(root, "done.json");
142
+ const manifestPath = existsSync(configuredManifestPath) ? configuredManifestPath : existsSync(fallbackManifestPath) ? fallbackManifestPath : null;
143
+ if (!manifestPath) {
144
+ return null;
145
+ }
146
+ const manifestRaw = await fs.readFile(manifestPath, "utf8");
147
+ const manifest = JSON.parse(manifestRaw);
148
+ return { root, manifestPath, configPath, manifest, config };
135
149
  }
136
150
  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 };
151
+ if (contractsPathOverride) {
152
+ const manifestPath = path.resolve(process.cwd(), contractsPathOverride);
153
+ const manifest = await readManifest(manifestPath);
154
+ return { manifestPath, manifestDir: path.dirname(manifestPath), manifest };
155
+ }
156
+ const workspace = await findWorkspace(process.cwd());
157
+ if (!workspace) {
158
+ throw new Error("Unable to find done.json. Run inside a Done workspace or pass --contracts <path>.");
159
+ }
160
+ return { manifestPath: workspace.manifestPath, manifestDir: path.dirname(workspace.manifestPath), manifest: workspace.manifest };
147
161
  }
148
162
  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;
163
+ let current = path.resolve(startDir);
164
+ while (true) {
165
+ const workspace = await detectWorkspace(current);
166
+ if (workspace) {
167
+ return workspace;
168
+ }
169
+ const parent = path.dirname(current);
170
+ if (parent === current) {
171
+ return null;
160
172
  }
173
+ current = parent;
174
+ }
161
175
  }
162
176
  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}`);
177
+ let raw;
178
+ try {
179
+ raw = await fs.readFile(manifestPath, "utf8");
180
+ } catch (error) {
181
+ if (error.code === "ENOENT") {
182
+ throw new Error(`Could not find ${manifestPath}. Pass a valid --contracts path or run from a Done workspace.`);
178
183
  }
184
+ throw error;
185
+ }
186
+ try {
187
+ return JSON.parse(raw);
188
+ } catch (error) {
189
+ throw new Error(`Failed to parse ${manifestPath}: ${error.message}`);
190
+ }
179
191
  }
180
192
  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");
193
+ const slug = toSlug(rawName);
194
+ if (!slug) {
195
+ throw new Error(`Could not derive a valid directory name from '${rawName}'.`);
196
+ }
197
+ const templateRef = options.template ?? DEFAULT_TEMPLATE_REPO;
198
+ const cwd = process.cwd();
199
+ const targetDir = path.resolve(cwd, slug);
200
+ await prepareTargetDir(targetDir, options.force === true);
201
+ const localTemplatePath = await resolveLocalTemplate(templateRef);
202
+ if (localTemplatePath) {
203
+ await fs.cp(localTemplatePath, targetDir, { recursive: true });
204
+ } else {
205
+ await runCommand("git", ["clone", "--depth", "1", templateRef, targetDir]);
206
+ }
207
+ await removeGitFolder(targetDir);
208
+ await updatePackageJson(path.join(targetDir, "package.json"), (pkg) => {
209
+ pkg.name = slug;
210
+ });
211
+ await updatePackageJson(path.join(targetDir, "frontend", "package.json"), (pkg) => {
212
+ pkg.name = `${slug}-frontend`;
213
+ });
214
+ if (options.install !== false) {
215
+ await runCommand("bun", ["install"], { cwd: targetDir });
216
+ }
217
+ console.log(`Done workspace created at ${path.relative(cwd, targetDir) || "."}.`);
218
+ console.log("Next steps:");
219
+ console.log(` cd ${path.relative(cwd, targetDir) || slug}`);
220
+ if (options.install === false) {
221
+ console.log(" bun install");
222
+ }
223
+ console.log(" bunx done dev");
213
224
  }
214
225
  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));
226
+ const slug = toSlug(rawName);
227
+ if (!slug) {
228
+ throw new Error(`Could not derive a valid contract directory name from '${rawName}'.`);
229
+ }
230
+ const contractName = toContractName(slug);
231
+ const contractsDir = resolveContractsDir(workspace.root, workspace.config.contractsDir);
232
+ const contractDir = path.join(contractsDir, slug);
233
+ await ensureDirAvailable(contractDir, options.force === true);
234
+ await fs.mkdir(path.join(contractDir, "src"), { recursive: true });
235
+ const pkgJsonPath = path.join(contractDir, "package.json");
236
+ const pkgJson = {
237
+ name: slug,
238
+ version: "0.1.0",
239
+ private: true,
240
+ type: "module",
241
+ scripts: {
242
+ typecheck: "tsc -p tsconfig.json --noEmit"
243
+ },
244
+ dependencies: {
245
+ "done.zone": "^0.1.0"
251
246
  }
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}");
247
+ };
248
+ await writeJson(pkgJsonPath, pkgJson);
249
+ const rootTsconfig = path.join(workspace.root, "tsconfig.json");
250
+ const tsconfig = {
251
+ compilerOptions: {
252
+ module: "ESNext",
253
+ moduleResolution: "bundler",
254
+ target: "ES2022",
255
+ isolatedModules: true,
256
+ noEmit: true
257
+ },
258
+ include: ["src/**/*.ts"]
259
+ };
260
+ if (existsSync(rootTsconfig)) {
261
+ tsconfig.extends = normalizeJsonPath(path.relative(contractDir, rootTsconfig));
262
+ }
263
+ await writeJson(path.join(contractDir, "tsconfig.json"), tsconfig);
264
+ const storeKey = `${slug}-greeting`;
265
+ const defaultGreeting = `Hello from ${contractName}`;
266
+ const contractSource = `const Greeting = Done.item<string>("${storeKey}");
256
267
 
257
268
  interface SetGreetingBody {
258
269
  greeting: string;
@@ -294,255 +305,736 @@ export default Done.serve()
294
305
  },
295
306
  );
296
307
  `;
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");
308
+ await fs.writeFile(path.join(contractDir, "src", "index.ts"), contractSource, "utf8");
309
+ workspace.manifest.contracts ??= [];
310
+ const contractDirRel = normalizeJsonPath(path.relative(workspace.root, contractDir));
311
+ const entry = ensureDotSlash(`${contractDirRel}/src/index.ts`);
312
+ const outFile = ensureDotSlash(`${contractDirRel}/dist/${slug}.contract.js`);
313
+ const watch = [ensureDotSlash(`${contractDirRel}/src/**/*.ts`)];
314
+ const newEntry = {
315
+ name: contractName,
316
+ entry,
317
+ outFile,
318
+ watch,
319
+ instantiate: { admin: null }
320
+ };
321
+ const existingIndex = workspace.manifest.contracts.findIndex((contract) => contract.name === contractName);
322
+ if (existingIndex >= 0 && options.force !== true) {
323
+ throw new Error(`Contract '${contractName}' already exists in done.json. Use --force to replace it.`);
324
+ }
325
+ if (existingIndex >= 0) {
326
+ workspace.manifest.contracts[existingIndex] = newEntry;
327
+ } else {
328
+ workspace.manifest.contracts.push(newEntry);
329
+ }
330
+ await writeJson(workspace.manifestPath, workspace.manifest);
331
+ console.log(`Created Done contract '${contractName}' in ${path.relative(workspace.root, contractDir)}`);
332
+ console.log("Next steps:");
333
+ console.log(" bunx done build --name", contractName);
334
+ console.log(" bunx done dev");
325
335
  }
326
336
  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
- }
337
+ const potentialPath = path.isAbsolute(reference) ? reference : path.resolve(process.cwd(), reference);
338
+ try {
339
+ const stats = await fs.stat(potentialPath);
340
+ if (stats.isDirectory()) {
341
+ return potentialPath;
333
342
  }
334
- catch { }
335
- return null;
343
+ } catch {
344
+ }
345
+ return null;
336
346
  }
337
347
  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 });
348
+ if (!existsSync(targetDir)) {
349
+ return;
350
+ }
351
+ if (!force) {
352
+ throw new Error(`Directory '${path.relative(process.cwd(), targetDir) || targetDir}' already exists. Use --force to overwrite.`);
353
+ }
354
+ await fs.rm(targetDir, { recursive: true, force: true });
345
355
  }
346
356
  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 });
357
+ if (!existsSync(targetDir)) {
358
+ return;
359
+ }
360
+ if (!force) {
361
+ throw new Error(`Directory '${path.relative(process.cwd(), targetDir) || targetDir}' already exists. Use --force to overwrite.`);
362
+ }
363
+ await fs.rm(targetDir, { recursive: true, force: true });
354
364
  }
355
365
  async function removeGitFolder(targetDir) {
356
- await fs.rm(path.join(targetDir, ".git"), { recursive: true, force: true });
366
+ await fs.rm(path.join(targetDir, ".git"), { recursive: true, force: true });
357
367
  }
358
368
  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);
369
+ if (!configured) {
370
+ return path.join(root, "contracts");
371
+ }
372
+ if (path.isAbsolute(configured)) {
373
+ return configured;
374
+ }
375
+ return path.resolve(root, configured);
366
376
  }
367
377
  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
+ try {
379
+ const current = JSON.parse(await fs.readFile(file, "utf8"));
380
+ mutator(current);
381
+ await writeJson(file, current);
382
+ } catch (error) {
383
+ if (error.code === "ENOENT") {
384
+ return;
378
385
  }
386
+ throw error;
387
+ }
379
388
  }
380
389
  function ensureDotSlash(value) {
381
- return value.startsWith("./") || value.startsWith("../") ? value : `./${value}`;
390
+ return value.startsWith("./") || value.startsWith("../") ? value : `./${value}`;
382
391
  }
383
392
  function normalizeJsonPath(relativePath) {
384
- const normalized = relativePath.split(path.sep).join("/");
385
- return normalized.startsWith("./") || normalized.startsWith("../") ? normalized : `./${normalized}`;
393
+ const normalized = relativePath.split(path.sep).join("/");
394
+ return normalized.startsWith("./") || normalized.startsWith("../") ? normalized : `./${normalized}`;
386
395
  }
387
396
  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)));
397
+ const candidates = [];
398
+ if (contract.name) {
399
+ candidates.push(normalizeMatchValue(contract.name));
400
+ }
401
+ if (contract.entry) {
402
+ candidates.push(normalizeMatchValue(contract.entry));
403
+ candidates.push(normalizeMatchValue(path.resolve(manifestDir, contract.entry)));
404
+ }
405
+ if (contract.outFile) {
406
+ candidates.push(normalizeMatchValue(contract.outFile));
407
+ candidates.push(normalizeMatchValue(path.resolve(manifestDir, contract.outFile)));
408
+ }
409
+ return matchers.some((matcher) => candidates.some((candidate) => matcher(candidate)));
401
410
  }
402
411
  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));
412
+ const normalized = normalizeMatchValue(pattern);
413
+ const hasWildcards = /[\*\?]/.test(normalized);
414
+ if (!hasWildcards) {
415
+ return (candidate) => normalizeMatchValue(candidate) === normalized;
416
+ }
417
+ const escaped = escapeRegExp(normalized).replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
418
+ const regex = new RegExp(`^${escaped}\$`);
419
+ return (candidate) => regex.test(normalizeMatchValue(candidate));
413
420
  }
414
421
  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;
422
+ if (!value) {
423
+ return "";
424
+ }
425
+ let normalized = value.replace(/\\/g, "/");
426
+ if (normalized.startsWith("./")) {
427
+ normalized = normalized.slice(2);
428
+ }
429
+ return normalized;
423
430
  }
424
431
  function escapeRegExp(value) {
425
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
432
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
426
433
  }
427
434
  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
- }
435
+ try {
436
+ const stats = await fs.stat(entryPath);
437
+ if (!stats.isFile()) {
438
+ const relPath = path.relative(process.cwd(), entryPath) || entryPath;
439
+ throw new Error(`Entry path for contract '${contractName}' is not a file (${relPath})`);
434
440
  }
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
+ } catch (error) {
442
+ if (error.code === "ENOENT") {
443
+ const rel = path.relative(process.cwd(), entryPath) || entryPath;
444
+ throw new Error(`Entry file for contract '${contractName}' not found (${rel})`);
445
+ }
446
+ throw error;
447
+ }
448
+ }
449
+ async function ensureFileExists(filePath, label) {
450
+ try {
451
+ const stats = await fs.stat(filePath);
452
+ if (!stats.isFile()) {
453
+ throw new Error(`The ${label} path is not a file (${filePath})`);
441
454
  }
455
+ } catch (error) {
456
+ if (error.code === "ENOENT") {
457
+ throw new Error(`The ${label} file does not exist (${filePath})`);
458
+ }
459
+ throw error;
460
+ }
461
+ }
462
+ async function loadMsgPayload(input) {
463
+ const potentialPath = path.resolve(process.cwd(), input);
464
+ if (await pathExists(potentialPath)) {
465
+ const raw = await fs.readFile(potentialPath, "utf8");
466
+ return JSON.parse(raw);
467
+ }
468
+ return JSON.parse(input);
469
+ }
470
+ function resolveDoneHttpBase(override) {
471
+ return override ?? process.env.DONE_HTTP ?? process.env.DONE_HTTP_BASE ?? "http://localhost:3000";
472
+ }
473
+ async function pathExists(candidate) {
474
+ try {
475
+ await fs.access(candidate);
476
+ return true;
477
+ } catch {
478
+ return false;
479
+ }
442
480
  }
443
481
  async function runBunBuild(entryPath, outFilePath) {
444
- const bun = getEmbeddedBunRuntime();
445
- if (bun) {
446
- const result = await bun.build({
447
- entrypoints: [entryPath],
448
- target: "bun",
449
- format: "esm",
450
- splitting: false,
451
- sourcemap: "none",
452
- external: ["bun:*", "node:*"],
453
- define: {
454
- "process.env.NODE_ENV": '"production"',
455
- },
456
- });
457
- if (!result.success) {
458
- const logs = (result.logs ?? []).map((log) => log?.message ?? String(log));
459
- throw new Error(logs.join("\n") || "Bun.build failed");
460
- }
461
- const [output] = result.outputs;
462
- if (!output) {
463
- throw new Error("Bun.build did not emit an output file");
464
- }
465
- const bundledSource = await output.text();
466
- await fs.writeFile(outFilePath, bundledSource);
467
- return;
482
+ const bun = getEmbeddedBunRuntime();
483
+ if (bun) {
484
+ const result = await bun.build({
485
+ entrypoints: [entryPath],
486
+ target: "bun",
487
+ format: "esm",
488
+ splitting: false,
489
+ sourcemap: "none",
490
+ external: ["bun:*", "node:*"],
491
+ define: {
492
+ "process.env.NODE_ENV": '"production"'
493
+ }
494
+ });
495
+ if (!result.success) {
496
+ const logs = (result.logs ?? []).map((log) => log?.message ?? String(log));
497
+ throw new Error(logs.join("\n") || "Bun.build failed");
498
+ }
499
+ const [output] = result.outputs;
500
+ if (!output) {
501
+ throw new Error("Bun.build did not emit an output file");
468
502
  }
469
- await runBunCliBuild(entryPath, outFilePath);
503
+ const bundledSource = await output.text();
504
+ await fs.writeFile(outFilePath, bundledSource);
505
+ return;
506
+ }
507
+ await runBunCliBuild(entryPath, outFilePath);
470
508
  }
471
509
  function getEmbeddedBunRuntime() {
472
- const bun = globalThis.Bun;
473
- if (!bun || typeof bun.build !== "function") {
474
- return null;
475
- }
476
- return bun;
510
+ const bun = globalThis.Bun;
511
+ if (!bun || typeof bun.build !== "function") {
512
+ return null;
513
+ }
514
+ return bun;
477
515
  }
478
516
  async function runBunCliBuild(entryPath, outFilePath) {
479
- const args = [
480
- "build",
481
- entryPath,
482
- "--outfile",
483
- outFilePath,
484
- "--target",
485
- "bun",
486
- "--format",
487
- "esm",
488
- "--sourcemap",
489
- "none",
490
- "--define",
491
- 'process.env.NODE_ENV="production"',
492
- ];
493
- for (const external of ["bun:*", "node:*"]) {
494
- args.push("--external", external);
495
- }
496
- try {
497
- await runCommand("bun", args);
498
- }
499
- catch (error) {
500
- if (error.code === "ENOENT") {
501
- throw new Error("Bun executable not found in PATH. Install Bun from https://bun.sh and re-run the build.");
502
- }
503
- throw error;
517
+ const args = [
518
+ "build",
519
+ entryPath,
520
+ "--outfile",
521
+ outFilePath,
522
+ "--target",
523
+ "bun",
524
+ "--format",
525
+ "esm",
526
+ "--sourcemap",
527
+ "none",
528
+ "--define",
529
+ 'process.env.NODE_ENV="production"'
530
+ ];
531
+ for (const external of ["bun:*", "node:*"]) {
532
+ args.push("--external", external);
533
+ }
534
+ try {
535
+ await runCommand("bun", args);
536
+ } catch (error) {
537
+ if (error.code === "ENOENT") {
538
+ throw new Error("Bun executable not found in PATH. Install Bun from https://bun.sh and re-run the build.");
504
539
  }
540
+ throw error;
541
+ }
505
542
  }
506
543
  function formatDuration(ms) {
507
- if (ms < 1000) {
508
- return `${ms}ms`;
509
- }
510
- return `${(ms / 1000).toFixed(2)}s`;
544
+ if (ms < 1000) {
545
+ return `${ms}ms`;
546
+ }
547
+ return `${(ms / 1000).toFixed(2)}s`;
511
548
  }
512
549
  function toSlug(input) {
513
- return input
514
- .trim()
515
- .toLowerCase()
516
- .replace(/[^a-z0-9]+/g, "-")
517
- .replace(/^-+/, "")
518
- .replace(/-+$/, "");
550
+ return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
519
551
  }
520
552
  function toContractName(slug) {
521
- const parts = slug.split(/[^a-zA-Z0-9]+/).filter(Boolean);
522
- if (parts.length === 0) {
523
- return "contract";
524
- }
525
- const [first, ...rest] = parts;
526
- return first + rest.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
553
+ const parts = slug.split(/[^a-zA-Z0-9]+/).filter(Boolean);
554
+ if (parts.length === 0) {
555
+ return "contract";
556
+ }
557
+ const [first, ...rest] = parts;
558
+ return first + rest.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
527
559
  }
528
560
  async function writeJson(file, data) {
529
- const json = JSON.stringify(data, null, 2);
530
- await fs.writeFile(file, `${json}\n`, "utf8");
561
+ const json = JSON.stringify(data, null, 2);
562
+ await fs.writeFile(file, `${json}\n`, "utf8");
531
563
  }
532
564
  async function runCommand(command, args, options = {}) {
533
- await new Promise((resolve, reject) => {
534
- const child = spawn(command, args, {
535
- cwd: options.cwd,
536
- stdio: "inherit",
537
- });
538
- child.on("close", (code) => {
539
- if (code === 0) {
540
- resolve();
541
- }
542
- else {
543
- reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}`));
544
- }
565
+ await new Promise((resolve, reject) => {
566
+ const child = spawn(command, args, {
567
+ cwd: options.cwd,
568
+ stdio: "inherit"
569
+ });
570
+ child.on("close", (code) => {
571
+ if (code === 0) {
572
+ resolve();
573
+ } else {
574
+ reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}`));
575
+ }
576
+ });
577
+ child.on("error", (error) => reject(error));
578
+ });
579
+ }
580
+ async function handleDev(options) {
581
+ const workspace = await findWorkspace(process.cwd());
582
+ if (!workspace) {
583
+ throw new Error("done dev must be run inside a Done workspace." + " Run `bunx done init` from your project root first.");
584
+ }
585
+ const startBackend = options.backend || !options.backend && !options.frontend;
586
+ const startFrontend = options.frontend || !options.backend && !options.frontend;
587
+ if (!startBackend && !startFrontend) {
588
+ throw new Error("Specify --backend and/or --frontend to start services.");
589
+ }
590
+ const devConfig = resolveDevConfig(workspace);
591
+ const orchestrator = new DoneDevOrchestrator({
592
+ workspace,
593
+ startBackend,
594
+ startFrontend,
595
+ devConfig
596
+ });
597
+ try {
598
+ await orchestrator.start();
599
+ await orchestrator.waitUntilStopped();
600
+ } finally {
601
+ await orchestrator.stop();
602
+ }
603
+ }
604
+ function resolveDevConfig(workspace) {
605
+ const dev = workspace.config.dev ?? {};
606
+ const host = dev.host ?? "127.0.0.1";
607
+ const port = dev.port ?? 8787;
608
+ const eventsHost = dev.eventsHost ?? host;
609
+ const eventsPort = dev.eventsPort ?? 7071;
610
+ const envFile = dev.envFile ? path.isAbsolute(dev.envFile) ? dev.envFile : path.resolve(workspace.root, dev.envFile) : path.resolve(workspace.root, ".done", "dev.env");
611
+ const frontendCwd = dev.frontend?.cwd ? path.isAbsolute(dev.frontend.cwd) ? dev.frontend.cwd : path.resolve(workspace.root, dev.frontend.cwd) : path.resolve(workspace.root, "frontend");
612
+ return {
613
+ envFile,
614
+ owner: dev.owner ?? "done1owner000000000000000000000000000000000000",
615
+ routerAddress: dev.routerAddress ?? null,
616
+ http: { host, port },
617
+ events: { host: eventsHost, port: eventsPort },
618
+ frontend: {
619
+ cwd: frontendCwd,
620
+ command: dev.frontend?.command ?? "bun run dev",
621
+ url: dev.frontend?.url ?? "http://127.0.0.1:5173",
622
+ open: dev.frontend?.open !== false
623
+ }
624
+ };
625
+ }
626
+ const DEV_LOG_PREFIX = pc.bold(pc.cyan("[done dev]"));
627
+ const DEV_WARN_PREFIX = pc.bold(pc.yellow("[done dev]"));
628
+ const DEV_ERROR_PREFIX = pc.bold(pc.red("[done dev]"));
629
+
630
+ class DoneDevOrchestrator {
631
+ options;
632
+ chain;
633
+ httpApp;
634
+ eventsServer;
635
+ watchers = [];
636
+ frontendProcess;
637
+ contracts = [];
638
+ envEntries = {};
639
+ httpUrl;
640
+ eventsUrl;
641
+ stopPromise;
642
+ resolveStop;
643
+ shuttingDown = false;
644
+ signalHandlers = [];
645
+ constructor(options) {
646
+ this.options = options;
647
+ }
648
+ async start() {
649
+ if (this.options.startBackend) {
650
+ await this.bootstrapBackend();
651
+ } else {
652
+ await this.loadEnvFromDisk();
653
+ }
654
+ if (this.options.startFrontend) {
655
+ await this.launchFrontend();
656
+ }
657
+ this.registerSignalHandlers();
658
+ this.printSummary();
659
+ if (this.options.startBackend || this.options.startFrontend) {
660
+ this.logInfo("Press Ctrl+C to stop");
661
+ }
662
+ }
663
+ async waitUntilStopped() {
664
+ if (!this.stopPromise) {
665
+ this.stopPromise = new Promise((resolve) => {
666
+ this.resolveStop = resolve;
667
+ });
668
+ }
669
+ return this.stopPromise;
670
+ }
671
+ async stop() {
672
+ if (this.shuttingDown) {
673
+ return this.stopPromise ?? Promise.resolve();
674
+ }
675
+ this.shuttingDown = true;
676
+ for (const { event, handler } of this.signalHandlers) {
677
+ process.off(event, handler);
678
+ }
679
+ this.signalHandlers = [];
680
+ await this.stopFrontend();
681
+ await Promise.all(this.watchers.map((watcher) => watcher.close().catch(() => {
682
+ })));
683
+ this.watchers = [];
684
+ await Promise.all(this.contracts.map((contract) => contract.queue.catch(() => {
685
+ })));
686
+ if (this.httpApp) {
687
+ await this.httpApp.stop().catch(() => {
688
+ });
689
+ this.httpApp = undefined;
690
+ }
691
+ if (this.eventsServer) {
692
+ try {
693
+ this.eventsServer.unsubscribe();
694
+ } catch {
695
+ }
696
+ await this.eventsServer.app.stop().catch(() => {
697
+ });
698
+ this.eventsServer = undefined;
699
+ }
700
+ this.chain = undefined;
701
+ this.resolveStop?.();
702
+ }
703
+ registerSignalHandlers() {
704
+ const shutdown = (signal) => {
705
+ this.logInfo(`Received ${signal}, shutting down...`);
706
+ this.stop();
707
+ };
708
+ for (const event of ["SIGINT", "SIGTERM"]) {
709
+ const handler = () => shutdown(event);
710
+ this.signalHandlers.push({ event, handler });
711
+ process.on(event, handler);
712
+ }
713
+ }
714
+ async bootstrapBackend() {
715
+ this.chain = new DoneLocalChain;
716
+ this.contracts = this.prepareContracts();
717
+ await this.buildAllContracts();
718
+ await this.deployContracts();
719
+ await this.startHttpServer();
720
+ await this.startEventsServer();
721
+ await this.writeEnvFile();
722
+ this.startWatchers();
723
+ }
724
+ prepareContracts() {
725
+ const manifestDir = path.dirname(this.options.workspace.manifestPath);
726
+ const contracts = this.options.workspace.manifest.contracts ?? [];
727
+ if (contracts.length === 0) {
728
+ throw new Error("No contracts defined in done.json. Run `bunx done init <name>` first.");
729
+ }
730
+ return contracts.map((contract) => {
731
+ if (!contract.entry) {
732
+ throw new Error(`Contract '${contract.name}' is missing an entry path.`);
733
+ }
734
+ if (!contract.outFile) {
735
+ throw new Error(`Contract '${contract.name}' is missing an outFile path.`);
736
+ }
737
+ const entryPath = path.resolve(manifestDir, contract.entry);
738
+ const outFilePath = path.resolve(manifestDir, contract.outFile);
739
+ const watchGlobs = (contract.watch && contract.watch.length > 0 ? contract.watch : [contract.entry]).map((pattern) => path.resolve(manifestDir, pattern));
740
+ return {
741
+ name: contract.name,
742
+ envKey: toEnvVarKey(contract.name),
743
+ entryPath,
744
+ outFilePath,
745
+ watchGlobs,
746
+ instantiateMsg: { ...contract.instantiate?.msg ?? {} },
747
+ admin: contract.instantiate?.admin ?? null,
748
+ label: contract.instantiate?.label,
749
+ publishMsg: contract.publish?.msg,
750
+ queue: Promise.resolve()
751
+ };
752
+ });
753
+ }
754
+ async buildAllContracts() {
755
+ for (const contract of this.contracts) {
756
+ await this.buildBundle(contract, true);
757
+ }
758
+ }
759
+ async buildBundle(contract, initial, reason) {
760
+ await ensureEntryExists(contract.entryPath, contract.name);
761
+ await fs.mkdir(path.dirname(contract.outFilePath), { recursive: true });
762
+ const startedAt = Date.now();
763
+ const label = initial ? "Building" : "Rebuilding";
764
+ this.logInfo(`${label} ${contract.name}${reason ? ` (${reason})` : ""}`);
765
+ await runBunBuild(contract.entryPath, contract.outFilePath);
766
+ this.logInfo(` ${pc.green("\u2713")} ${contract.name} bundle ready (${formatDuration(Date.now() - startedAt)})`);
767
+ }
768
+ async deployContracts() {
769
+ if (!this.chain)
770
+ return;
771
+ for (const contract of this.contracts) {
772
+ const deployment = await deployJsContract({
773
+ chain: this.chain,
774
+ sender: this.options.devConfig.owner,
775
+ scriptPath: contract.outFilePath,
776
+ instantiateMsg: contract.instantiateMsg,
777
+ cwJsCodeId: this.chain.batteries?.cwJs.codeId,
778
+ admin: contract.admin ?? undefined,
779
+ label: contract.label ?? contract.name
780
+ });
781
+ if (deployment.result.err) {
782
+ throw new Error(`Failed to deploy ${contract.name}: ${deployment.result.err}`);
783
+ }
784
+ contract.address = deployment.address;
785
+ this.chain.advanceBlock();
786
+ this.logInfo(`${pc.green("\u2022")} ${contract.name} deployed at ${deployment.address}`);
787
+ }
788
+ }
789
+ async startHttpServer() {
790
+ if (!this.chain)
791
+ return;
792
+ const { host, port } = this.options.devConfig.http;
793
+ const httpServer = await createDoneHttpLocalServer({
794
+ port,
795
+ owner: this.options.devConfig.owner,
796
+ routerAddress: this.options.devConfig.routerAddress ?? undefined
797
+ }, this.chain);
798
+ httpServer.app.listen({ port, hostname: host });
799
+ this.httpApp = httpServer.app;
800
+ this.httpUrl = `http://${host}:${port}`;
801
+ this.logInfo(`done-http-local listening on ${this.httpUrl}`);
802
+ }
803
+ async startEventsServer() {
804
+ if (!this.chain)
805
+ return;
806
+ const { host, port } = this.options.devConfig.events;
807
+ const eventsServer = createDoneEventsLocalServer({ chain: this.chain });
808
+ eventsServer.app.listen({ port, hostname: host });
809
+ this.eventsServer = eventsServer;
810
+ this.eventsUrl = `http://${host}:${port}`;
811
+ this.logInfo(`done-events-local listening on ${this.eventsUrl}`);
812
+ }
813
+ startWatchers() {
814
+ for (const contract of this.contracts) {
815
+ const watcher = chokidar.watch(contract.watchGlobs, { ignoreInitial: true });
816
+ watcher.on("all", (event, filePath) => {
817
+ const reason = `${event} ${this.formatWorkspacePath(filePath)}`;
818
+ this.enqueueContractUpdate(contract, reason);
819
+ });
820
+ watcher.on("error", (error) => {
821
+ this.logError(`Watcher error for ${contract.name}`, error);
822
+ });
823
+ this.watchers.push(watcher);
824
+ }
825
+ }
826
+ enqueueContractUpdate(contract, reason) {
827
+ contract.queue = contract.queue.catch(() => {
828
+ }).then(async () => {
829
+ try {
830
+ await this.buildBundle(contract, false, reason);
831
+ await this.publishContract(contract);
832
+ } catch (error) {
833
+ this.logError(`Failed to refresh ${contract.name}`, error);
834
+ }
835
+ });
836
+ }
837
+ async publishContract(contract) {
838
+ if (!this.chain || !contract.address)
839
+ return;
840
+ const script = await fs.readFile(contract.outFilePath, "utf8");
841
+ const execResult = this.chain.execute(contract.address, {
842
+ publish_code: {
843
+ script,
844
+ ...contract.publishMsg ? { msg: contract.publishMsg } : {}
845
+ }
846
+ }, {
847
+ sender: this.options.devConfig.owner,
848
+ funds: []
849
+ });
850
+ if (execResult.result.err) {
851
+ throw new Error(execResult.result.err);
852
+ }
853
+ this.chain.advanceBlock();
854
+ await this.writeEnvFile({ DONE_DEV_UPDATED_AT: new Date().toISOString() });
855
+ this.logInfo(`${pc.green("\u21BB")} updated ${contract.name} (height ${this.chain.height})`);
856
+ }
857
+ async launchFrontend() {
858
+ const frontend = this.options.devConfig.frontend;
859
+ if (!existsSync(frontend.cwd)) {
860
+ this.logWarn(`Skipping frontend; directory not found (${frontend.cwd})`);
861
+ return;
862
+ }
863
+ if (Object.keys(this.envEntries).length === 0) {
864
+ await this.loadEnvFromDisk();
865
+ }
866
+ const env = { ...process.env, ...this.envEntries };
867
+ if (this.httpUrl) {
868
+ env.VITE_DONE_HTTP_URL = this.httpUrl;
869
+ env.VITE_DONE_HTTP = this.httpUrl;
870
+ env.VITE_DONE_HTTP_BASE = this.httpUrl;
871
+ }
872
+ if (this.eventsUrl) {
873
+ env.VITE_DONE_EVENTS_URL = this.eventsUrl;
874
+ }
875
+ for (const [key, value] of Object.entries(this.envEntries)) {
876
+ if (key.startsWith("DONE_CONTRACT_")) {
877
+ env[`VITE_${key}`] = value;
878
+ }
879
+ }
880
+ this.logInfo(`Starting frontend (${frontend.command})`);
881
+ this.frontendProcess = spawn(frontend.command, {
882
+ cwd: frontend.cwd,
883
+ env,
884
+ stdio: "inherit",
885
+ shell: true
886
+ });
887
+ this.frontendProcess.on("exit", (code) => {
888
+ if (!this.shuttingDown && code !== 0) {
889
+ this.logError(`Frontend exited with code ${code ?? 0}`);
890
+ }
891
+ });
892
+ if (frontend.open) {
893
+ setTimeout(() => {
894
+ openBrowser(frontend.url).catch((error) => {
895
+ this.logWarn(`Failed to open browser: ${error.message}`);
545
896
  });
546
- child.on("error", (error) => reject(error));
897
+ }, 1500);
898
+ }
899
+ }
900
+ async stopFrontend() {
901
+ if (!this.frontendProcess) {
902
+ return;
903
+ }
904
+ await new Promise((resolve) => {
905
+ const child = this.frontendProcess;
906
+ const timeout = setTimeout(() => {
907
+ try {
908
+ child.kill("SIGKILL");
909
+ } catch {
910
+ }
911
+ resolve();
912
+ }, 1000);
913
+ child.once("exit", () => {
914
+ clearTimeout(timeout);
915
+ resolve();
916
+ });
917
+ try {
918
+ child.kill("SIGINT");
919
+ } catch {
920
+ resolve();
921
+ }
922
+ this.frontendProcess = undefined;
547
923
  });
924
+ }
925
+ async writeEnvFile(extra = {}) {
926
+ const env = {
927
+ ...extra
928
+ };
929
+ if (this.httpUrl) {
930
+ env.DONE_HTTP_URL = this.httpUrl;
931
+ env.DONE_HTTP_BASE = this.httpUrl;
932
+ env.DONE_HTTP_LOCAL = this.httpUrl;
933
+ }
934
+ if (this.eventsUrl) {
935
+ env.DONE_EVENTS_URL = this.eventsUrl;
936
+ env.DONE_EVENTS_LOCAL = this.eventsUrl;
937
+ }
938
+ env.DONE_OWNER_ADDRESS = this.options.devConfig.owner;
939
+ env.DONE_DEV_UPDATED_AT = new Date().toISOString();
940
+ for (const contract of this.contracts) {
941
+ if (contract.address) {
942
+ env[`DONE_CONTRACT_${contract.envKey}`] = contract.address;
943
+ }
944
+ }
945
+ this.envEntries = env;
946
+ await writeDevEnvFile(this.options.devConfig.envFile, env);
947
+ }
948
+ async loadEnvFromDisk() {
949
+ try {
950
+ this.envEntries = await readDevEnvFile(this.options.devConfig.envFile);
951
+ } catch (error) {
952
+ this.logWarn(`Unable to read ${this.options.devConfig.envFile}: ${error.message}`);
953
+ this.envEntries = {};
954
+ }
955
+ }
956
+ printSummary() {
957
+ this.logInfo(pc.bold("Local stack ready"));
958
+ if (this.httpUrl) {
959
+ console.log(` ${pc.dim("http")}: ${this.httpUrl}`);
960
+ }
961
+ if (this.eventsUrl) {
962
+ console.log(` ${pc.dim("events")}: ${this.eventsUrl}`);
963
+ }
964
+ if (this.contracts.length > 0) {
965
+ console.log(" Contracts:");
966
+ for (const contract of this.contracts) {
967
+ if (contract.address) {
968
+ console.log(` - ${contract.name}: ${contract.address}`);
969
+ }
970
+ }
971
+ }
972
+ const relEnv = path.relative(process.cwd(), this.options.devConfig.envFile) || this.options.devConfig.envFile;
973
+ console.log(` Env file: ${relEnv}`);
974
+ if (this.options.startFrontend) {
975
+ console.log(` Frontend: ${this.options.devConfig.frontend.url}`);
976
+ }
977
+ }
978
+ formatWorkspacePath(filePath) {
979
+ return path.relative(this.options.workspace.root, filePath) || filePath;
980
+ }
981
+ logInfo(message) {
982
+ console.log(DEV_LOG_PREFIX, message);
983
+ }
984
+ logWarn(message) {
985
+ console.warn(DEV_WARN_PREFIX, message);
986
+ }
987
+ logError(message, error) {
988
+ if (error) {
989
+ console.error(DEV_ERROR_PREFIX, `${message}: ${error.message}`);
990
+ } else {
991
+ console.error(DEV_ERROR_PREFIX, message);
992
+ }
993
+ }
994
+ }
995
+ async function writeDevEnvFile(file, entries) {
996
+ await fs.mkdir(path.dirname(file), { recursive: true });
997
+ const lines = ["# generated by done dev", `# ${new Date().toISOString()}`];
998
+ for (const key of Object.keys(entries).sort()) {
999
+ lines.push(`${key}=${entries[key]}`);
1000
+ }
1001
+ await fs.writeFile(file, `${lines.join("\n")}\n`, "utf8");
1002
+ }
1003
+ async function readDevEnvFile(file) {
1004
+ const data = await fs.readFile(file, "utf8");
1005
+ const entries = {};
1006
+ for (const line of data.split(/\r?\n/)) {
1007
+ if (!line || line.startsWith("#"))
1008
+ continue;
1009
+ const [key, ...rest] = line.split("=");
1010
+ if (!key || rest.length === 0)
1011
+ continue;
1012
+ entries[key.trim()] = rest.join("=").trim();
1013
+ }
1014
+ return entries;
1015
+ }
1016
+ function toEnvVarKey(name) {
1017
+ return name.replace(/[^a-zA-Z0-9]+/g, "_").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
1018
+ }
1019
+ function openBrowser(url) {
1020
+ return new Promise((resolve, reject) => {
1021
+ const platform = process.platform;
1022
+ let child;
1023
+ try {
1024
+ if (platform === "darwin") {
1025
+ child = spawn("open", [url], { stdio: "ignore", detached: true });
1026
+ } else if (platform === "win32") {
1027
+ child = spawn("cmd", ["/c", "start", "", url], {
1028
+ stdio: "ignore",
1029
+ detached: true
1030
+ });
1031
+ } else {
1032
+ child = spawn("xdg-open", [url], { stdio: "ignore", detached: true });
1033
+ }
1034
+ child.unref();
1035
+ resolve();
1036
+ } catch (error) {
1037
+ reject(error);
1038
+ }
1039
+ });
548
1040
  }