@tscircuit/cli 0.0.3 → 0.0.5

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/bun.lockb CHANGED
Binary file
package/dist/cli.js CHANGED
@@ -210,7 +210,7 @@ import {Command} from "commander";
210
210
  // package.json
211
211
  var package_default = {
212
212
  name: "@tscircuit/cli",
213
- version: "0.0.2",
213
+ version: "0.0.5",
214
214
  private: false,
215
215
  type: "module",
216
216
  description: "Command line tool for developing, publishing and installing tscircuit circuits",
@@ -221,11 +221,13 @@ var package_default = {
221
221
  "start:dev-server": "bun build:dev-server && bun cli.ts dev -y --cwd ./tests/assets/example-project",
222
222
  "build:dev-server": "cd dev-server-api && bun run build && cd ../dev-server-frontend && bun run build",
223
223
  "build:cli": "bun build-cli.ts",
224
- build: "bun build:dev-server && npm run build:cli"
224
+ build: "bun build:dev-server && npm run build:cli",
225
+ "test:init": "bun cli.ts init --dir ./tmp/test --name test"
225
226
  },
226
227
  bin: {
227
228
  tscircuit: "./dist/cli.js",
228
- tsci: "./dist/cli.js"
229
+ tsci: "./dist/cli.js",
230
+ tsck: "./dist/cli.js"
229
231
  },
230
232
  keywords: [],
231
233
  author: "",
@@ -237,6 +239,7 @@ var package_default = {
237
239
  "@tscircuit/react-fiber": "^1.0.6",
238
240
  axios: "^1.6.7",
239
241
  "better-sqlite3": "^9.4.3",
242
+ chokidar: "^3.6.0",
240
243
  commander: "^12.0.0",
241
244
  configstore: "^6.0.0",
242
245
  "dax-sh": "^0.39.2",
@@ -250,13 +253,14 @@ var package_default = {
250
253
  minimist: "^1.2.8",
251
254
  "node-persist": "^4.0.1",
252
255
  open: "^10.1.0",
253
- "perfect-cli": "1.0.12",
256
+ "perfect-cli": "1.0.13",
254
257
  prompts: "^2.4.2",
255
258
  react: "^18.2.0",
256
259
  zod: "latest"
257
260
  },
258
261
  devDependencies: {
259
262
  "@types/bun": "^1.0.8",
263
+ "@types/chokidar": "^2.1.3",
260
264
  "@types/configstore": "^6.0.2",
261
265
  "@types/lodash": "^4.14.202",
262
266
  "@types/mime-types": "^2.1.4",
@@ -273,7 +277,7 @@ var package_default = {
273
277
 
274
278
  // lib/cmd-fns/config-reveal-location.ts
275
279
  var configRevealLocation = async (ctx, args) => {
276
- console.log(`tsci config path: ${ctx.global_config.path}`);
280
+ console.log(`tsck config path: ${ctx.global_config.path}`);
277
281
  };
278
282
  // lib/cmd-fns/config-set-registry.ts
279
283
  import {z} from "zod";
@@ -619,10 +623,9 @@ var soupifyCmd = async (ctx, args) => {
619
623
  console.log(JSON.stringify(soup, null, 2));
620
624
  }
621
625
  };
622
- // lib/cmd-fns/dev.ts
626
+ // lib/cmd-fns/dev/index.ts
623
627
  import {z as z16} from "zod";
624
- import kleur4 from "kleur";
625
- import {join as joinPath} from "path";
628
+ import kleur7 from "kleur";
626
629
  import prompts from "prompts";
627
630
  import open from "open";
628
631
 
@@ -844,66 +847,54 @@ var bundle_default = {
844
847
  "index.html": Buffer.from("PCFkb2N0eXBlIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KICA8aGVhZD4KICAgIDxtZXRhIGNoYXJzZXQ9IlVURi04IiAvPgogICAgPCEtLSA8bGluayByZWw9Imljb24iIHR5cGU9ImltYWdlL3N2Zyt4bWwiIGhyZWY9Ii92aXRlLnN2ZyIgLz4gLS0+CiAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEuMCIgLz4KICAgIDx0aXRsZT5bZGV2XSB0c2NpcmN1aXQ8L3RpdGxlPgogICAgPHNjcmlwdCB0eXBlPSJtb2R1bGUiIGNyb3Nzb3JpZ2luIHNyYz0iL3ByZXZpZXcvYXNzZXRzL2luZGV4LUNpRUdEejdkLmpzIj48L3NjcmlwdD4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgY3Jvc3NvcmlnaW4gaHJlZj0iL3ByZXZpZXcvYXNzZXRzL2luZGV4LUNNNkk3TGhOLmNzcyI+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPGRpdiBpZD0icm9vdCI+PC9kaXY+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==", "base64")
845
848
  };
846
849
 
847
- // lib/cmd-fns/dev.ts
848
- import defaultAxios from "axios";
849
- import {readdirSync, readFileSync} from "fs";
850
+ // lib/cmd-fns/dev/dev-server-request-handler.ts
850
851
  import EdgeRuntimePrimitives from "@edge-runtime/primitives";
851
852
  import mime from "mime-types";
852
- var devCmd = async (ctx, args) => {
853
- const params = z16.object({
854
- cwd: z16.string().optional().default(process.cwd()),
855
- port: z16.coerce.number().optional().default(3020)
856
- }).parse(args);
857
- const { cwd, port } = params;
858
- console.log(kleur4.green(`\n--------------------------------------------\n\nStarting dev server http://localhost:${port}\n\n--------------------------------------------\n\n`));
859
- const serverUrl = `http://localhost:${port}`;
860
- const axios = defaultAxios.create({
861
- baseURL: serverUrl
862
- });
863
- axios.interceptors.response.use((res) => res, (err) => {
864
- console.log(kleur4.red(`[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${err.config.url}\n\n${JSON.stringify(err.response?.data, null, " ")}`.replace(/\\n/g, "\n").replace(/\\"/g, '"')));
865
- console.log(kleur4.yellow("[Request Body]:"), err.config.data);
866
- return Promise.reject(err);
867
- });
868
- const requestHandler = async (bunReq) => {
869
- const url = new URL(bunReq.url);
870
- const requestType = url.pathname.startsWith("/api") ? "api" : url.pathname.startsWith("/preview") ? "preview" : "other";
871
- if (requestType === "api") {
872
- const req = new EdgeRuntimePrimitives.Request(bunReq.url, {
873
- headers: bunReq.headers,
874
- method: bunReq.method,
875
- body: bunReq.body
876
- });
877
- const response = await stdin_default.makeRequest(req, {});
878
- return response;
879
- } else if (requestType === "preview") {
880
- let frontendPath = url.pathname.replace("/preview", "");
881
- if (frontendPath === "/" || frontendPath === "") {
882
- frontendPath = "index.html";
853
+ var devServerRequestHandler = async (bunReq) => {
854
+ const url = new URL(bunReq.url);
855
+ const requestType = url.pathname.startsWith("/api") ? "api" : url.pathname.startsWith("/preview") ? "preview" : "other";
856
+ if (requestType === "api") {
857
+ const req = new EdgeRuntimePrimitives.Request(bunReq.url, {
858
+ headers: bunReq.headers,
859
+ method: bunReq.method,
860
+ body: bunReq.body
861
+ });
862
+ const response = await stdin_default.makeRequest(req, {});
863
+ return response;
864
+ } else if (requestType === "preview") {
865
+ let frontendPath = url.pathname.replace("/preview", "");
866
+ if (frontendPath === "/" || frontendPath === "") {
867
+ frontendPath = "index.html";
868
+ }
869
+ frontendPath = frontendPath.replace(/^\//, "");
870
+ const fileContent = bundle_default[frontendPath];
871
+ if (!fileContent) {
872
+ return new Response("Not Found", { status: 404 });
873
+ }
874
+ return new Response(fileContent.toString("utf-8"), {
875
+ headers: {
876
+ "Content-Type": mime.lookup(frontendPath) || "text/plain"
883
877
  }
884
- frontendPath = frontendPath.replace(/^\//, "");
885
- const fileContent = bundle_default[frontendPath];
886
- if (!fileContent) {
887
- return new Response("Not Found", { status: 404 });
878
+ });
879
+ } else {
880
+ return new Response(null, {
881
+ status: 302,
882
+ headers: {
883
+ Location: "/preview"
888
884
  }
889
- return new Response(fileContent.toString("utf-8"), {
890
- headers: {
891
- "Content-Type": mime.lookup(frontendPath) || "text/plain"
892
- }
893
- });
894
- } else {
895
- return new Response(null, {
896
- status: 302,
897
- headers: {
898
- Location: "/preview"
899
- }
900
- });
901
- }
902
- };
885
+ });
886
+ }
887
+ };
888
+
889
+ // lib/cmd-fns/dev/start-dev-server.ts
890
+ var startDevServer = async ({
891
+ port,
892
+ devServerAxios
893
+ }) => {
903
894
  let server;
904
895
  if (typeof Bun !== "undefined") {
905
896
  server = Bun.serve({
906
- fetch: requestHandler,
897
+ fetch: devServerRequestHandler,
907
898
  development: false,
908
899
  port
909
900
  });
@@ -911,49 +902,155 @@ var devCmd = async (ctx, args) => {
911
902
  const { Hono } = await import("hono");
912
903
  const { serve } = await import("@hono/node-server");
913
904
  const honoApp = new Hono;
914
- honoApp.all("/*", (c) => requestHandler(c.req.raw));
905
+ honoApp.all("/*", (c) => devServerRequestHandler(c.req.raw));
915
906
  server = serve({
916
907
  fetch: honoApp.fetch,
917
908
  port
918
909
  });
919
910
  }
920
911
  console.log("Running health check against dev server...");
921
- await axios.get("/api/health");
922
- console.log(`Loading examples...`);
923
- const examplesDir = joinPath(cwd, "examples");
924
- const exampleFileNames = readdirSync(examplesDir);
912
+ await devServerAxios.get("/api/health");
913
+ return server;
914
+ };
915
+
916
+ // lib/cmd-fns/dev/get-dev-server-axios.ts
917
+ import kleur4 from "kleur";
918
+ import defaultAxios from "axios";
919
+ var getDevServerAxios = ({ serverUrl }) => {
920
+ const devServerAxios = defaultAxios.create({
921
+ baseURL: serverUrl
922
+ });
923
+ devServerAxios.interceptors.response.use((res) => res, (err) => {
924
+ console.log(kleur4.red(`[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${err.config.url}\n\n${JSON.stringify(err.response?.data, null, " ")}`.replace(/\\n/g, "\n").replace(/\\"/g, '"')));
925
+ console.log(kleur4.yellow("[Request Body]:"), err.config.data);
926
+ return Promise.reject(err);
927
+ });
928
+ return devServerAxios;
929
+ };
930
+
931
+ // lib/cmd-fns/dev/upload-examples-from-directory.ts
932
+ import {join as joinPath2} from "path";
933
+ import {readdirSync as readdirSync2} from "fs";
934
+
935
+ // lib/cmd-fns/dev/soupify-and-upload-example-file.ts
936
+ import kleur5 from "kleur";
937
+ import {join as joinPath} from "path";
938
+ import {readFileSync} from "fs";
939
+ var soupifyAndUploadExampleFile = async ({
940
+ examplesDir,
941
+ exampleFileName,
942
+ devServerAxios
943
+ }) => {
944
+ try {
945
+ const examplePath = joinPath(examplesDir, exampleFileName);
946
+ const exampleContent = readFileSync(examplePath).toString();
947
+ let exportName = "default";
948
+ if (!exampleContent.includes("export default")) {
949
+ const matches = Array.from(exampleContent.matchAll(/export\s+(const|function)\s+([A-Z]\w+)\s*=?/g)).map((m) => m[2]);
950
+ if (matches.length === 0) {
951
+ throw new Error(`No export detected in "${exampleFileName}"`);
952
+ }
953
+ if (matches.length > 1) {
954
+ throw new Error(`Multiple exports detected in "${exampleFileName}", only single exports currently working`);
955
+ }
956
+ exportName = matches[0];
957
+ }
958
+ console.log(kleur5.gray(`[soupifying] ${exampleFileName}...`));
959
+ const soup = await soupify({
960
+ filePath: examplePath,
961
+ exportName
962
+ });
963
+ console.log(kleur5.gray(`[uploading ] ${exampleFileName}...`));
964
+ await devServerAxios.post("/api/dev_package_examples/create", {
965
+ tscircuit_soup: soup,
966
+ file_path: examplePath,
967
+ export_name: exportName
968
+ });
969
+ console.log(kleur5.gray(`[ done ] ${exampleFileName}!`));
970
+ } catch (e) {
971
+ console.log(kleur5.red(e.toString()));
972
+ }
973
+ };
974
+
975
+ // lib/cmd-fns/dev/upload-examples-from-directory.ts
976
+ var uploadExamplesFromDirectory = async ({
977
+ cwd,
978
+ devServerAxios
979
+ }) => {
980
+ const examplesDir = joinPath2(cwd, "examples");
981
+ const exampleFileNames = readdirSync2(examplesDir);
925
982
  for (const exampleFileName of exampleFileNames) {
926
- try {
927
- const examplePath = joinPath(examplesDir, exampleFileName);
928
- const exampleContent = readFileSync(examplePath).toString();
929
- let exportName = "default";
930
- if (!exampleContent.includes("export default")) {
931
- const matches = Array.from(exampleContent.matchAll(/export\s+(const|function)\s+([A-Z]\w+)\s*=?/g)).map((m) => m[2]);
932
- if (matches.length === 0) {
933
- throw new Error(`No export detected in "${exampleFileName}"`);
934
- }
935
- if (matches.length > 1) {
936
- throw new Error(`Multiple exports detected in "${exampleFileName}", only single exports currently working`);
937
- }
938
- exportName = matches[0];
983
+ if (exampleFileName.endsWith(".__tmp_entrypoint.tsx"))
984
+ continue;
985
+ if (!exampleFileName.endsWith(".tsx"))
986
+ continue;
987
+ await soupifyAndUploadExampleFile({
988
+ devServerAxios,
989
+ examplesDir,
990
+ exampleFileName
991
+ });
992
+ }
993
+ };
994
+
995
+ // lib/cmd-fns/dev/index.ts
996
+ import {unlink as unlink2} from "fs/promises";
997
+ import * as Path3 from "path";
998
+
999
+ // lib/cmd-fns/dev/start-watcher.ts
1000
+ import chokidar from "chokidar";
1001
+ import kleur6 from "kleur";
1002
+ var startWatcher = async ({
1003
+ cwd,
1004
+ devServerAxios
1005
+ }) => {
1006
+ const watcher = chokidar.watch(`${cwd}/**/*.tsx`, {
1007
+ ignored: /node_modules/,
1008
+ persistent: true
1009
+ });
1010
+ const upload_queue_state = {
1011
+ dirty: false,
1012
+ should_run: true
1013
+ };
1014
+ watcher.on("change", async (path) => {
1015
+ console.log(`File ${path} has been changed`);
1016
+ upload_queue_state.dirty = true;
1017
+ });
1018
+ async function uploadInBackground() {
1019
+ while (upload_queue_state.should_run) {
1020
+ if (upload_queue_state.dirty) {
1021
+ console.log(kleur6.yellow("Changes detected, re-uploading examples..."));
1022
+ upload_queue_state.dirty = false;
1023
+ await uploadExamplesFromDirectory({ cwd, devServerAxios });
939
1024
  }
940
- console.log("Soupifying...");
941
- const soup = await soupify({
942
- filePath: examplePath,
943
- exportName
944
- });
945
- console.log(`Soupified ${exampleFileName}!`);
946
- console.log(`Uploading ${exampleFileName}...`);
947
- await axios.post("/api/dev_package_examples/create", {
948
- tscircuit_soup: soup,
949
- file_path: examplePath,
950
- export_name: exportName
951
- });
952
- console.log(`Uploaded ${exampleFileName}!`);
953
- } catch (e) {
954
- console.log(kleur4.red(e.toString()));
1025
+ await new Promise((resolve) => setTimeout(resolve, 100));
955
1026
  }
956
1027
  }
1028
+ const _backgroundUploaderPromise = uploadInBackground();
1029
+ return {
1030
+ _backgroundUploaderPromise,
1031
+ stop: () => {
1032
+ upload_queue_state.should_run = false;
1033
+ watcher.close();
1034
+ }
1035
+ };
1036
+ };
1037
+
1038
+ // lib/cmd-fns/dev/index.ts
1039
+ var devCmd = async (ctx, args) => {
1040
+ const params = z16.object({
1041
+ cwd: z16.string().optional().default(process.cwd()),
1042
+ port: z16.coerce.number().optional().default(3020)
1043
+ }).parse(args);
1044
+ const { cwd, port } = params;
1045
+ unlink2(Path3.join(cwd, ".tscircuit/dev-server.db")).catch(() => {
1046
+ });
1047
+ console.log(kleur7.green(`\n--------------------------------------------\n\nStarting dev server http://localhost:${port}\n\n--------------------------------------------\n\n`));
1048
+ const serverUrl = `http://localhost:${port}`;
1049
+ const devServerAxios = getDevServerAxios({ serverUrl });
1050
+ const server = await startDevServer({ port, devServerAxios });
1051
+ console.log(`Loading examples...`);
1052
+ await uploadExamplesFromDirectory({ devServerAxios, cwd });
1053
+ const watcher = await startWatcher({ cwd, devServerAxios });
957
1054
  while (true) {
958
1055
  const { action } = await prompts({
959
1056
  type: "select",
@@ -977,13 +1074,151 @@ var devCmd = async (ctx, args) => {
977
1074
  server.stop();
978
1075
  if (server.close)
979
1076
  server.close();
1077
+ watcher.stop();
980
1078
  break;
981
1079
  }
982
1080
  }
983
1081
  };
1082
+ // lib/cmd-fns/init.ts
1083
+ import {z as z17} from "zod";
1084
+ import {
1085
+ readFileSync as readFileSync3,
1086
+ writeFileSync as writeFileSync3,
1087
+ existsSync,
1088
+ mkdirSync as mkdirSync2,
1089
+ appendFileSync
1090
+ } from "fs";
1091
+ import * as Path4 from "node:path";
1092
+ import $2 from "dax-sh";
1093
+ var initCmd = async (ctx, args) => {
1094
+ const params = z17.object({
1095
+ name: z17.string().optional(),
1096
+ dir: z17.string().optional(),
1097
+ runtime: z17.enum(["bun", "node"]).optional()
1098
+ }).parse(args);
1099
+ if (params.name && !params.dir) {
1100
+ params.dir = `./${params.name.split("/").pop()}`;
1101
+ } else if (!params.dir) {
1102
+ params.dir = `.`;
1103
+ }
1104
+ let runtime = params.runtime;
1105
+ if (!runtime) {
1106
+ const bunExists = $2.commandExistsSync("bun");
1107
+ if (bunExists) {
1108
+ runtime = "bun";
1109
+ } else {
1110
+ runtime = "node";
1111
+ }
1112
+ }
1113
+ const pkm = runtime === "bun" ? "bun" : "npm";
1114
+ const runner = runtime === "bun" ? "bun" : "tsx";
1115
+ const packageJsonExists = existsSync(Path4.join(params.dir, "package.json"));
1116
+ const dirExists = existsSync(params.dir);
1117
+ if (!dirExists) {
1118
+ console.log(`Creating directory ${params.dir}`);
1119
+ mkdirSync2(params.dir, { recursive: true });
1120
+ }
1121
+ process.chdir(params.dir);
1122
+ if (!packageJsonExists) {
1123
+ console.log(`Initializing ${pkm} project...`);
1124
+ await $2`${pkm} init -y`;
1125
+ }
1126
+ if (runtime !== "bun") {
1127
+ console.log(`Setting up project for typescript...`);
1128
+ await $2`${pkm} add -D typescript tsx`;
1129
+ }
1130
+ await $2`${pkm} add -D @tscircuit/react-fiber @tscircuit/builder`;
1131
+ console.log("Changing package name...");
1132
+ const packageJson = JSON.parse(readFileSync3("package.json", "utf-8"));
1133
+ packageJson.name = params.name;
1134
+ writeFileSync3("package.json", JSON.stringify(packageJson, null, 2));
1135
+ console.log(`Adding ".tscircuit" to .gitignore`);
1136
+ appendFileSync(".gitignore", "\n.tscircuit\n*.__tmp_entrypoint.tsx", {
1137
+ encoding: "utf-8",
1138
+ flag: "a+"
1139
+ });
1140
+ console.log("Creating lib and examples directories...");
1141
+ mkdirSync2("examples", { recursive: true });
1142
+ mkdirSync2("lib", { recursive: true });
1143
+ writeFileSync3(Path4.join("lib", "MyCircuit.tsx"), `
1144
+ export const MyCircuit = () => (
1145
+ <resistor
1146
+ resistance="10kohm"
1147
+ name="R1"
1148
+ footprint="0805"
1149
+ />
1150
+ )
1151
+ `.trim());
1152
+ writeFileSync3("index.ts", `export * from "./lib/MyCircuit"`);
1153
+ writeFileSync3(Path4.join("examples", "MyExample.tsx"), `
1154
+ import { MyCircuit } from "lib/MyCircuit"
1155
+
1156
+ export const MyExample = () => (
1157
+ <MyCircuit />
1158
+ )
1159
+ `.trim());
1160
+ writeFileSync3("README.md", `
1161
+ # ${params.name}
1162
+
1163
+ > This project was generated using [tsck](https://github.com/tscircuit/tscircuit)
1164
+
1165
+ To develop and view the examples, run \`tsck dev\` and open [http://localhost:3020](http://localhost:3020) in your browser.
1166
+
1167
+ ## Developing
1168
+
1169
+ You should install an editor like [VS Code](https://code.visualstudio.com/) with a typescript extension. Many web developers already have this.
1170
+
1171
+ Usually, you'll want to develop some named circuits inside of the \`lib\` directory,
1172
+ export them in the \`index.ts\` file, then show how to use them in the \`examples\` directory.
1173
+
1174
+ Any file in \`examples\` will be automatically loaded and appear in the browser preview when you run \`tsck dev\`. It auto-reloads when you make changes, no need to reload the page or re-run the command.
1175
+
1176
+ > [!TIP] Make sure to replace this README with some details about your project and how to use it.
1177
+
1178
+ ## Publishing
1179
+
1180
+ After you're satisfied with your project, publish it on the [tscircuit registry](https://registry.tscircuit.com) with \`tsck publish\`
1181
+
1182
+
1183
+
1184
+ `.trim());
1185
+ writeFileSync3("tsconfig.json", `
1186
+ {
1187
+ "compilerOptions": {
1188
+ // Enable latest features
1189
+ "lib": ["ESNext"],
1190
+ "target": "ESNext",
1191
+ "module": "ESNext",
1192
+ "moduleDetection": "force",
1193
+ "jsx": "react-jsx",
1194
+ "allowJs": true,
1195
+ "types": ["@tscircuit/react-fiber"],
1196
+ "baseUrl": ".",
1197
+
1198
+ // Bundler mode
1199
+ "moduleResolution": "bundler",
1200
+ "allowImportingTsExtensions": true,
1201
+ "verbatimModuleSyntax": true,
1202
+ "noEmit": true,
1203
+
1204
+ // Best practices
1205
+ "strict": true,
1206
+ "skipLibCheck": true,
1207
+ "noFallthroughCasesInSwitch": true,
1208
+
1209
+ // Some stricter flags (disabled by default)
1210
+ "noUnusedLocals": false,
1211
+ "noUnusedParameters": false,
1212
+ "noPropertyAccessFromIndexSignature": false
1213
+ }
1214
+ }
1215
+
1216
+ `.trim());
1217
+ console.log("Done! Run `tsck dev` to start developing!");
1218
+ };
984
1219
  // lib/get-program.ts
985
1220
  var getProgram = (ctx) => {
986
- const cmd = new Command("tsci");
1221
+ const cmd = new Command("tsck");
987
1222
  cmd.version(package_default.version);
988
1223
  const authCmd = cmd.command("auth");
989
1224
  authCmd.command("login").action((args) => authLogin(ctx, args));
@@ -1017,11 +1252,12 @@ var getProgram = (ctx) => {
1017
1252
  packageExamples.command("get").requiredOption("--package-example-id <package_example_id>").action((args) => packageExamplesGet(ctx, args));
1018
1253
  packageExamples.command("create").requiredOption("--package-name-with-version <package_name_with_version>").requiredOption("--file <file>").option("--export <export>", "Name of export to soupify").action((args) => packageExamplesCreate(ctx, args));
1019
1254
  cmd.command("publish").action((args) => publish(ctx, args));
1020
- cmd.command("version").action(() => console.log(`tsci v${package_default.version}`));
1255
+ cmd.command("version").action(() => console.log(`tsck v${package_default.version}`));
1021
1256
  cmd.command("login").action((args) => authLogin(ctx, args));
1022
1257
  cmd.command("logout").action((args) => authLogout(ctx, args));
1023
1258
  cmd.command("soupify").requiredOption("--file <file>", "Input example files").option("--export <export_name>", "Name of export to soupify, if not specified, soupify the default/only export").action((args) => soupifyCmd(ctx, args));
1024
1259
  cmd.command("dev").option("--cwd <cwd>", "Current working directory").option("--port <port>", "Port to run dev server on").action((args) => devCmd(ctx, args));
1260
+ cmd.command("init").option("--name <name>", "Name of the project").option("--runtime <runtime>", "Runtime to use (attempts to bun, otherwise node/tsx)").option("--dir <dir>", "Directory to initialize (defaults to ./<name> or . if name not provided)").action((args) => initCmd(ctx, args));
1025
1261
  cmd.command("add").action((args) => {
1026
1262
  });
1027
1263
  cmd.command("remove").action((args) => {
@@ -1033,7 +1269,7 @@ var getProgram = (ctx) => {
1033
1269
 
1034
1270
  // lib/util/create-context-and-run-program.ts
1035
1271
  import defaultAxios2 from "axios";
1036
- import kleur5 from "kleur";
1272
+ import kleur8 from "kleur";
1037
1273
 
1038
1274
  // lib/param-handlers/interact-for-local-directory.ts
1039
1275
  import * as fs4 from "fs/promises";
@@ -1246,6 +1482,7 @@ var interactForPackageName = async ({
1246
1482
  var PARAM_HANDLERS_BY_PARAM_NAME = {
1247
1483
  file: interactForLocalFile,
1248
1484
  cwd: interactForLocalDirectory,
1485
+ dir: interactForLocalDirectory,
1249
1486
  package_release_id: interactForPackageReleaseId,
1250
1487
  package_name: interactForPackageName,
1251
1488
  package_name_with_version: interactForPackageNameWithVersion,
@@ -1255,7 +1492,7 @@ var PARAM_HANDLERS_BY_PARAM_NAME = {
1255
1492
  // lib/util/create-context-and-run-program.ts
1256
1493
  var createContextAndRunProgram = async (process_args) => {
1257
1494
  const args = minimist(process_args);
1258
- const global_config = new Configstore("tsci");
1495
+ const global_config = new Configstore("tsck");
1259
1496
  const current_profile = args.profile ?? global_config.get("current_profile") ?? "default";
1260
1497
  const profile_config = {
1261
1498
  get: (key) => global_config.get(`profiles.${current_profile}.${key}`),
@@ -1278,17 +1515,17 @@ var createContextAndRunProgram = async (process_args) => {
1278
1515
  if (global_config.get("log_requests")) {
1279
1516
  console.log(`Using registry_url: ${registry_url}`);
1280
1517
  axios.interceptors.request.use((req) => {
1281
- console.log(kleur5.grey(`[REQ] ${req.method?.toUpperCase()} ${req.url}`));
1518
+ console.log(kleur8.grey(`[REQ] ${req.method?.toUpperCase()} ${req.url}`));
1282
1519
  return req;
1283
1520
  });
1284
1521
  axios.interceptors.response.use((res) => {
1285
- console.log(kleur5.grey(`[RES] ${res.status} ${res.config.method?.toUpperCase()} ${res.config.url}`));
1522
+ console.log(kleur8.grey(`[RES] ${res.status} ${res.config.method?.toUpperCase()} ${res.config.url}`));
1286
1523
  return res;
1287
1524
  });
1288
1525
  }
1289
1526
  axios.interceptors.response.use((res) => res, (err) => {
1290
- console.log(kleur5.red(`[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${err.config.url}\n\n${JSON.stringify(err.response?.data, null, " ")}`.replace(/\\n/g, "\n").replace(/\\"/g, '"')));
1291
- console.log(kleur5.yellow("[Request Body]:"), err.config.data);
1527
+ console.log(kleur8.red(`[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${err.config.url}\n\n${JSON.stringify(err.response?.data, null, " ")}`.replace(/\\n/g, "\n").replace(/\\"/g, '"')));
1528
+ console.log(kleur8.yellow("[Request Body]:"), err.config.data);
1292
1529
  return Promise.reject(err);
1293
1530
  });
1294
1531
  const ctx = {
@@ -0,0 +1,54 @@
1
+ import apiServer from "../../../dev-server-api/dist/bundle"
2
+ import frontendVfs from "../../../dev-server-frontend/dist/bundle"
3
+ import EdgeRuntimePrimitives from "@edge-runtime/primitives"
4
+ import mime from "mime-types"
5
+
6
+ export const devServerRequestHandler = async (bunReq: Request) => {
7
+ const url = new URL(bunReq.url)
8
+ const requestType = url.pathname.startsWith("/api")
9
+ ? "api"
10
+ : url.pathname.startsWith("/preview")
11
+ ? "preview"
12
+ : "other"
13
+
14
+ if (requestType === "api") {
15
+ // We have to shim Bun's Request until they fix the issue where
16
+ // .clone() doesn't clone the body
17
+ // https://github.com/oven-sh/bun/pull/8668
18
+ const req = new EdgeRuntimePrimitives.Request(bunReq.url, {
19
+ headers: bunReq.headers,
20
+ method: bunReq.method,
21
+ body: bunReq.body,
22
+ })
23
+
24
+ const response = await apiServer.makeRequest(req, {})
25
+ return response
26
+ } else if (requestType === "preview") {
27
+ let frontendPath = url.pathname.replace("/preview", "")
28
+ if (frontendPath === "/" || frontendPath === "") {
29
+ frontendPath = "index.html"
30
+ }
31
+ frontendPath = frontendPath.replace(/^\//, "")
32
+
33
+ const fileContent: Buffer = (frontendVfs as any)[frontendPath]
34
+ if (!fileContent) {
35
+ return new Response("Not Found", { status: 404 })
36
+ }
37
+ return new Response(
38
+ // Buffer.from(fileContent.toString(), "base64").toString("utf-8"),
39
+ fileContent.toString("utf-8"),
40
+ {
41
+ headers: {
42
+ "Content-Type": mime.lookup(frontendPath) || "text/plain",
43
+ },
44
+ }
45
+ )
46
+ } else {
47
+ return new Response(null, {
48
+ status: 302,
49
+ headers: {
50
+ Location: "/preview",
51
+ },
52
+ })
53
+ }
54
+ }
@@ -0,0 +1,25 @@
1
+ import kleur from "kleur"
2
+ import defaultAxios from "axios"
3
+
4
+ export const getDevServerAxios = ({ serverUrl }: { serverUrl: string }) => {
5
+ const devServerAxios = defaultAxios.create({
6
+ baseURL: serverUrl,
7
+ })
8
+ devServerAxios.interceptors.response.use(
9
+ (res) => res,
10
+ (err) => {
11
+ console.log(
12
+ kleur.red(
13
+ `[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${
14
+ err.config.url
15
+ }\n\n${JSON.stringify(err.response?.data, null, " ")}`
16
+ .replace(/\\n/g, "\n")
17
+ .replace(/\\"/g, '"')
18
+ )
19
+ )
20
+ console.log(kleur.yellow("[Request Body]:"), err.config.data)
21
+ return Promise.reject(err)
22
+ }
23
+ )
24
+ return devServerAxios
25
+ }
@@ -1,16 +1,15 @@
1
1
  import { AppContext } from "../../util/app-context"
2
2
  import { z } from "zod"
3
3
  import kleur from "kleur"
4
- import { join as joinPath } from "path"
5
4
  import prompts from "prompts"
6
5
  import open from "open"
7
- import apiServer from "../../../dev-server-api/dist/bundle"
8
- import frontendVfs from "../../../dev-server-frontend/dist/bundle"
9
- import defaultAxios from "axios"
10
- import { readdirSync, readFileSync } from "fs"
11
- import { soupify } from "lib/soupify"
12
- import EdgeRuntimePrimitives from "@edge-runtime/primitives"
13
- import mime from "mime-types"
6
+ import { startDevServer } from "./start-dev-server"
7
+ import { getDevServerAxios } from "./get-dev-server-axios"
8
+ import { uploadExamplesFromDirectory } from "./upload-examples-from-directory"
9
+ import { unlink } from "fs/promises"
10
+ import * as Path from "path"
11
+ import { appendFileSync } from "fs"
12
+ import { startWatcher } from "./start-watcher"
14
13
 
15
14
  export const devCmd = async (ctx: AppContext, args: any) => {
16
15
  const params = z
@@ -24,7 +23,8 @@ export const devCmd = async (ctx: AppContext, args: any) => {
24
23
 
25
24
  // Load package.json, install tscircuit if it's not installed. If any other
26
25
  // tscircuit dependency like @tscircuit/builder is installed, then don't
27
- // install anything
26
+ // install anything. If there is nothing in the current directory, ask to
27
+ // instead run "tsck init"
28
28
  // TODO
29
29
 
30
30
  // Check that examples directory exists, if not create it and put inside
@@ -37,13 +37,8 @@ export const devCmd = async (ctx: AppContext, args: any) => {
37
37
  // Add .tscircuit to .gitignore if it's not already there
38
38
  // TODO
39
39
 
40
- // Delete and recreate .tscircuit/dev-server.db
41
-
42
- // INSIDE BACKGROUND SERVICE
43
- // Soupify contents of examples directory, upload to sqlite db
44
- // TODO
45
- // Watch and re-soupify on file changes
46
- // TODO
40
+ // Delete old .tscircuit/dev-server.db
41
+ unlink(Path.join(cwd, ".tscircuit/dev-server.db")).catch(() => {})
47
42
 
48
43
  console.log(
49
44
  kleur.green(
@@ -51,144 +46,16 @@ export const devCmd = async (ctx: AppContext, args: any) => {
51
46
  )
52
47
  )
53
48
  const serverUrl = `http://localhost:${port}`
54
- const axios = defaultAxios.create({
55
- baseURL: serverUrl,
56
- })
57
- axios.interceptors.response.use(
58
- (res) => res,
59
- (err) => {
60
- console.log(
61
- kleur.red(
62
- `[ERR] ${err.response?.status} ${err.config.method?.toUpperCase()} ${
63
- err.config.url
64
- }\n\n${JSON.stringify(err.response?.data, null, " ")}`
65
- .replace(/\\n/g, "\n")
66
- .replace(/\\"/g, '"')
67
- )
68
- )
69
- console.log(kleur.yellow("[Request Body]:"), err.config.data)
70
- return Promise.reject(err)
71
- }
72
- )
73
-
74
- const requestHandler = async (bunReq: Request) => {
75
- const url = new URL(bunReq.url)
76
- const requestType = url.pathname.startsWith("/api")
77
- ? "api"
78
- : url.pathname.startsWith("/preview")
79
- ? "preview"
80
- : "other"
49
+ const devServerAxios = getDevServerAxios({ serverUrl })
81
50
 
82
- if (requestType === "api") {
83
- // We have to shim Bun's Request until they fix the issue where
84
- // .clone() doesn't clone the body
85
- // https://github.com/oven-sh/bun/pull/8668
86
- const req = new EdgeRuntimePrimitives.Request(bunReq.url, {
87
- headers: bunReq.headers,
88
- method: bunReq.method,
89
- body: bunReq.body,
90
- })
91
-
92
- const response = await apiServer.makeRequest(req, {})
93
- return response
94
- } else if (requestType === "preview") {
95
- let frontendPath = url.pathname.replace("/preview", "")
96
- if (frontendPath === "/" || frontendPath === "") {
97
- frontendPath = "index.html"
98
- }
99
- frontendPath = frontendPath.replace(/^\//, "")
100
-
101
- const fileContent: Buffer = (frontendVfs as any)[frontendPath]
102
- if (!fileContent) {
103
- return new Response("Not Found", { status: 404 })
104
- }
105
- return new Response(
106
- // Buffer.from(fileContent.toString(), "base64").toString("utf-8"),
107
- fileContent.toString("utf-8"),
108
- {
109
- headers: {
110
- "Content-Type": mime.lookup(frontendPath) || "text/plain",
111
- },
112
- }
113
- )
114
- } else {
115
- return new Response(null, {
116
- status: 302,
117
- headers: {
118
- Location: "/preview",
119
- },
120
- })
121
- }
122
- }
123
-
124
- let server: any
125
- if (typeof Bun !== "undefined") {
126
- server = Bun.serve({
127
- fetch: requestHandler,
128
- development: false,
129
- port,
130
- })
131
- } else {
132
- // Hono messes up the globals, only import it if we don't have Bun
133
- const { Hono } = await import("hono")
134
- const { serve } = await import("@hono/node-server")
135
- const honoApp = new Hono()
136
- honoApp.all("/*", (c) => requestHandler(c.req.raw))
137
- server = serve({
138
- fetch: honoApp.fetch,
139
- port,
140
- })
141
- }
142
-
143
- console.log("Running health check against dev server...")
144
- await axios.get("/api/health")
51
+ const server = await startDevServer({ port, devServerAxios })
145
52
 
146
53
  // Soupify all examples
147
54
  console.log(`Loading examples...`)
148
- const examplesDir = joinPath(cwd, "examples")
149
- const exampleFileNames = readdirSync(examplesDir)
150
- for (const exampleFileName of exampleFileNames) {
151
- try {
152
- const examplePath = joinPath(examplesDir, exampleFileName)
153
- const exampleContent = readFileSync(examplePath).toString()
154
-
155
- let exportName = "default"
156
-
157
- if (!exampleContent.includes("export default")) {
158
- // try to infer an export if possible
159
- const matches = Array.from(
160
- exampleContent.matchAll(
161
- /export\s+(const|function)\s+([A-Z]\w+)\s*=?/g
162
- )
163
- ).map((m) => m[2])
164
- if (matches.length === 0) {
165
- throw new Error(`No export detected in "${exampleFileName}"`)
166
- }
167
- if (matches.length > 1) {
168
- throw new Error(
169
- `Multiple exports detected in "${exampleFileName}", only single exports currently working`
170
- )
171
- }
172
- exportName = matches[0]
173
- }
55
+ await uploadExamplesFromDirectory({ devServerAxios, cwd })
174
56
 
175
- console.log("Soupifying...")
176
- const soup = await soupify({
177
- filePath: examplePath,
178
- exportName,
179
- })
180
- console.log(`Soupified ${exampleFileName}!`)
181
- console.log(`Uploading ${exampleFileName}...`)
182
- await axios.post("/api/dev_package_examples/create", {
183
- tscircuit_soup: soup,
184
- file_path: examplePath,
185
- export_name: exportName,
186
- })
187
- console.log(`Uploaded ${exampleFileName}!`)
188
- } catch (e: any) {
189
- console.log(kleur.red(e.toString()))
190
- }
191
- }
57
+ // Start watcher
58
+ const watcher = await startWatcher({ cwd, devServerAxios })
192
59
 
193
60
  while (true) {
194
61
  const { action } = await prompts({
@@ -211,6 +78,7 @@ export const devCmd = async (ctx: AppContext, args: any) => {
211
78
  } else if (!action || action === "stop") {
212
79
  if (server.stop) server.stop()
213
80
  if (server.close) server.close()
81
+ watcher.stop()
214
82
  break
215
83
  }
216
84
  }
@@ -0,0 +1,53 @@
1
+ import kleur from "kleur"
2
+ import { join as joinPath } from "path"
3
+ import { AxiosInstance } from "axios"
4
+ import { readdirSync, readFileSync } from "fs"
5
+ import { soupify } from "lib/soupify"
6
+
7
+ export const soupifyAndUploadExampleFile = async ({
8
+ examplesDir,
9
+ exampleFileName,
10
+ devServerAxios,
11
+ }: {
12
+ examplesDir: string
13
+ exampleFileName: string
14
+ devServerAxios: AxiosInstance
15
+ }) => {
16
+ try {
17
+ const examplePath = joinPath(examplesDir, exampleFileName)
18
+ const exampleContent = readFileSync(examplePath).toString()
19
+
20
+ let exportName = "default"
21
+
22
+ if (!exampleContent.includes("export default")) {
23
+ // try to infer an export if possible
24
+ const matches = Array.from(
25
+ exampleContent.matchAll(/export\s+(const|function)\s+([A-Z]\w+)\s*=?/g)
26
+ ).map((m) => m[2])
27
+ if (matches.length === 0) {
28
+ throw new Error(`No export detected in "${exampleFileName}"`)
29
+ }
30
+ if (matches.length > 1) {
31
+ throw new Error(
32
+ `Multiple exports detected in "${exampleFileName}", only single exports currently working`
33
+ )
34
+ }
35
+ exportName = matches[0]
36
+ }
37
+
38
+ console.log(kleur.gray(`[soupifying] ${exampleFileName}...`))
39
+ const soup = await soupify({
40
+ filePath: examplePath,
41
+ exportName,
42
+ })
43
+ console.log(kleur.gray(`[uploading ] ${exampleFileName}...`))
44
+ await devServerAxios.post("/api/dev_package_examples/create", {
45
+ tscircuit_soup: soup,
46
+ file_path: examplePath,
47
+ export_name: exportName,
48
+ })
49
+ console.log(kleur.gray(`[ done ] ${exampleFileName}!`))
50
+ } catch (e: any) {
51
+ console.log(kleur.red(e.toString()))
52
+ }
53
+ }
@@ -0,0 +1,34 @@
1
+ import { AxiosInstance } from "axios"
2
+ import { devServerRequestHandler } from "./dev-server-request-handler"
3
+
4
+ export const startDevServer = async ({
5
+ port,
6
+ devServerAxios,
7
+ }: {
8
+ port: number
9
+ devServerAxios: AxiosInstance
10
+ }) => {
11
+ let server: any
12
+ if (typeof Bun !== "undefined") {
13
+ server = Bun.serve({
14
+ fetch: devServerRequestHandler,
15
+ development: false,
16
+ port,
17
+ })
18
+ } else {
19
+ // Hono messes up the globals, only import it if we don't have Bun
20
+ const { Hono } = await import("hono")
21
+ const { serve } = await import("@hono/node-server")
22
+ const honoApp = new Hono()
23
+ honoApp.all("/*", (c) => devServerRequestHandler(c.req.raw))
24
+ server = serve({
25
+ fetch: honoApp.fetch,
26
+ port,
27
+ })
28
+ }
29
+
30
+ console.log("Running health check against dev server...")
31
+ await devServerAxios.get("/api/health")
32
+
33
+ return server
34
+ }
@@ -0,0 +1,48 @@
1
+ import { AxiosInstance } from "axios"
2
+ import chokidar from "chokidar"
3
+ import { uploadExamplesFromDirectory } from "./upload-examples-from-directory"
4
+ import kleur from "kleur"
5
+
6
+ export const startWatcher = async ({
7
+ cwd,
8
+ devServerAxios,
9
+ }: {
10
+ cwd: string
11
+ devServerAxios: AxiosInstance
12
+ }) => {
13
+ const watcher = chokidar.watch(`${cwd}/**/*.tsx`, {
14
+ ignored: /node_modules/,
15
+ persistent: true,
16
+ })
17
+
18
+ const upload_queue_state = {
19
+ dirty: false,
20
+ should_run: true,
21
+ }
22
+ watcher.on("change", async (path) => {
23
+ console.log(`File ${path} has been changed`)
24
+ // TODO analyze to determine which examples were impacted
25
+ upload_queue_state.dirty = true
26
+ })
27
+
28
+ async function uploadInBackground() {
29
+ while (upload_queue_state.should_run) {
30
+ if (upload_queue_state.dirty) {
31
+ console.log(kleur.yellow("Changes detected, re-uploading examples..."))
32
+ upload_queue_state.dirty = false
33
+ await uploadExamplesFromDirectory({ cwd, devServerAxios })
34
+ }
35
+ await new Promise((resolve) => setTimeout(resolve, 100))
36
+ }
37
+ }
38
+
39
+ const _backgroundUploaderPromise = uploadInBackground()
40
+
41
+ return {
42
+ _backgroundUploaderPromise,
43
+ stop: () => {
44
+ upload_queue_state.should_run = false
45
+ watcher.close()
46
+ },
47
+ }
48
+ }
@@ -0,0 +1,26 @@
1
+ import kleur from "kleur"
2
+ import { join as joinPath } from "path"
3
+ import { AxiosInstance } from "axios"
4
+ import { readdirSync, readFileSync } from "fs"
5
+ import { soupify } from "lib/soupify"
6
+ import { soupifyAndUploadExampleFile } from "./soupify-and-upload-example-file"
7
+
8
+ export const uploadExamplesFromDirectory = async ({
9
+ cwd,
10
+ devServerAxios,
11
+ }: {
12
+ cwd: string
13
+ devServerAxios: AxiosInstance
14
+ }) => {
15
+ const examplesDir = joinPath(cwd, "examples")
16
+ const exampleFileNames = readdirSync(examplesDir)
17
+ for (const exampleFileName of exampleFileNames) {
18
+ if (exampleFileName.endsWith(".__tmp_entrypoint.tsx")) continue
19
+ if (!exampleFileName.endsWith(".tsx")) continue
20
+ await soupifyAndUploadExampleFile({
21
+ devServerAxios,
22
+ examplesDir,
23
+ exampleFileName,
24
+ })
25
+ }
26
+ }
@@ -24,4 +24,5 @@ export { packageExamplesGet } from "./package-examples-get"
24
24
  export { packageExamplesCreate } from "./package-examples-create"
25
25
  export { publish } from "./publish"
26
26
  export { soupifyCmd as soupify } from "./soupify"
27
- export { devCmd as dev } from "./dev/dev"
27
+ export { devCmd as dev } from "./dev"
28
+ export { initCmd as init } from "./init"
@@ -0,0 +1,175 @@
1
+ import { AppContext } from "../util/app-context"
2
+ import { z } from "zod"
3
+ import {
4
+ readFileSync,
5
+ writeFileSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ writeFile,
9
+ appendFileSync,
10
+ } from "fs"
11
+ import * as Path from "node:path"
12
+ import $ from "dax-sh"
13
+
14
+ export const initCmd = async (ctx: AppContext, args: any) => {
15
+ const params = z
16
+ .object({
17
+ name: z.string().optional(),
18
+ dir: z.string().optional(),
19
+ runtime: z.enum(["bun", "node"]).optional(),
20
+ })
21
+ .parse(args)
22
+
23
+ if (params.name && !params.dir) {
24
+ params.dir = `./${params.name.split("/").pop()}`
25
+ } else if (!params.dir) {
26
+ params.dir = `.`
27
+ }
28
+
29
+ let runtime = params.runtime
30
+ if (!runtime) {
31
+ const bunExists = $.commandExistsSync("bun")
32
+
33
+ if (bunExists) {
34
+ runtime = "bun"
35
+ } else {
36
+ runtime = "node"
37
+ }
38
+ }
39
+
40
+ const pkm = runtime === "bun" ? "bun" : "npm"
41
+ const runner = runtime === "bun" ? "bun" : "tsx"
42
+
43
+ const packageJsonExists = existsSync(Path.join(params.dir, "package.json"))
44
+ const dirExists = existsSync(params.dir)
45
+
46
+ if (!dirExists) {
47
+ console.log(`Creating directory ${params.dir}`)
48
+ mkdirSync(params.dir, { recursive: true })
49
+ }
50
+
51
+ process.chdir(params.dir)
52
+ if (!packageJsonExists) {
53
+ console.log(`Initializing ${pkm} project...`)
54
+ await $`${pkm} init -y`
55
+ }
56
+
57
+ if (runtime !== "bun") {
58
+ console.log(`Setting up project for typescript...`)
59
+ await $`${pkm} add -D typescript tsx`
60
+ }
61
+
62
+ // TODO just allow adding "tscircuit" in the future
63
+ await $`${pkm} add -D @tscircuit/react-fiber @tscircuit/builder`
64
+
65
+ console.log("Changing package name...")
66
+ // Change package.json "name" to params.name
67
+ const packageJson = JSON.parse(readFileSync("package.json", "utf-8"))
68
+ packageJson.name = params.name
69
+ writeFileSync("package.json", JSON.stringify(packageJson, null, 2))
70
+
71
+ console.log(`Adding ".tscircuit" to .gitignore`)
72
+ appendFileSync(".gitignore", "\n.tscircuit\n*.__tmp_entrypoint.tsx", {
73
+ encoding: "utf-8",
74
+ flag: "a+",
75
+ })
76
+
77
+ console.log("Creating lib and examples directories...")
78
+ mkdirSync("examples", { recursive: true })
79
+ mkdirSync("lib", { recursive: true })
80
+
81
+ writeFileSync(
82
+ Path.join("lib", "MyCircuit.tsx"),
83
+ `
84
+ export const MyCircuit = () => (
85
+ <resistor
86
+ resistance="10kohm"
87
+ name="R1"
88
+ footprint="0805"
89
+ />
90
+ )
91
+ `.trim()
92
+ )
93
+
94
+ writeFileSync("index.ts", `export * from "./lib/MyCircuit"`)
95
+
96
+ writeFileSync(
97
+ Path.join("examples", "MyExample.tsx"),
98
+ `
99
+ import { MyCircuit } from "lib/MyCircuit"
100
+
101
+ export const MyExample = () => (
102
+ <MyCircuit />
103
+ )
104
+ `.trim()
105
+ )
106
+
107
+ // Override the README file
108
+ writeFileSync(
109
+ "README.md",
110
+ `
111
+ # ${params.name}
112
+
113
+ > This project was generated using [tsck](https://github.com/tscircuit/tscircuit)
114
+
115
+ To develop and view the examples, run \`tsck dev\` and open [http://localhost:3020](http://localhost:3020) in your browser.
116
+
117
+ ## Developing
118
+
119
+ You should install an editor like [VS Code](https://code.visualstudio.com/) with a typescript extension. Many web developers already have this.
120
+
121
+ Usually, you'll want to develop some named circuits inside of the \`lib\` directory,
122
+ export them in the \`index.ts\` file, then show how to use them in the \`examples\` directory.
123
+
124
+ Any file in \`examples\` will be automatically loaded and appear in the browser preview when you run \`tsck dev\`. It auto-reloads when you make changes, no need to reload the page or re-run the command.
125
+
126
+ > [!TIP] Make sure to replace this README with some details about your project and how to use it.
127
+
128
+ ## Publishing
129
+
130
+ After you're satisfied with your project, publish it on the [tscircuit registry](https://registry.tscircuit.com) with \`tsck publish\`
131
+
132
+
133
+
134
+ `.trim()
135
+ )
136
+
137
+ // Open tsconfig.json and modify it to import the tscircuit types
138
+ writeFileSync(
139
+ "tsconfig.json",
140
+ `
141
+ {
142
+ "compilerOptions": {
143
+ // Enable latest features
144
+ "lib": ["ESNext"],
145
+ "target": "ESNext",
146
+ "module": "ESNext",
147
+ "moduleDetection": "force",
148
+ "jsx": "react-jsx",
149
+ "allowJs": true,
150
+ "types": ["@tscircuit/react-fiber"],
151
+ "baseUrl": ".",
152
+
153
+ // Bundler mode
154
+ "moduleResolution": "bundler",
155
+ "allowImportingTsExtensions": true,
156
+ "verbatimModuleSyntax": true,
157
+ "noEmit": true,
158
+
159
+ // Best practices
160
+ "strict": true,
161
+ "skipLibCheck": true,
162
+ "noFallthroughCasesInSwitch": true,
163
+
164
+ // Some stricter flags (disabled by default)
165
+ "noUnusedLocals": false,
166
+ "noUnusedParameters": false,
167
+ "noPropertyAccessFromIndexSignature": false
168
+ }
169
+ }
170
+
171
+ `.trim()
172
+ )
173
+
174
+ console.log("Done! Run `tsck dev` to start developing!")
175
+ }
@@ -183,6 +183,19 @@ export const getProgram = (ctx: AppContext) => {
183
183
  .option("--port <port>", "Port to run dev server on")
184
184
  .action((args) => CMDFN.dev(ctx, args))
185
185
 
186
+ cmd
187
+ .command("init")
188
+ .option("--name <name>", "Name of the project")
189
+ .option(
190
+ "--runtime <runtime>",
191
+ "Runtime to use (attempts to bun, otherwise node/tsx)"
192
+ )
193
+ .option(
194
+ "--dir <dir>",
195
+ "Directory to initialize (defaults to ./<name> or . if name not provided)"
196
+ )
197
+ .action((args) => CMDFN.init(ctx, args))
198
+
186
199
  cmd.command("add").action((args) => {})
187
200
  cmd.command("remove").action((args) => {})
188
201
  cmd.command("install").action((args) => {})
@@ -9,6 +9,7 @@ import { ParamHandler } from "./param-handler-type"
9
9
  export const PARAM_HANDLERS_BY_PARAM_NAME: Record<string, ParamHandler> = {
10
10
  file: interactForLocalFile,
11
11
  cwd: interactForLocalDirectory,
12
+ dir: interactForLocalDirectory,
12
13
  package_release_id: interactForPackageReleaseId,
13
14
  package_name: interactForPackageName,
14
15
  package_name_with_version: interactForPackageNameWithVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Command line tool for developing, publishing and installing tscircuit circuits",
@@ -11,7 +11,8 @@
11
11
  "start:dev-server": "bun build:dev-server && bun cli.ts dev -y --cwd ./tests/assets/example-project",
12
12
  "build:dev-server": "cd dev-server-api && bun run build && cd ../dev-server-frontend && bun run build",
13
13
  "build:cli": "bun build-cli.ts",
14
- "build": "bun build:dev-server && npm run build:cli"
14
+ "build": "bun build:dev-server && npm run build:cli",
15
+ "test:init": "bun cli.ts init --dir ./tmp/test --name test"
15
16
  },
16
17
  "bin": {
17
18
  "tscircuit": "./dist/cli.js",
@@ -28,6 +29,7 @@
28
29
  "@tscircuit/react-fiber": "^1.0.6",
29
30
  "axios": "^1.6.7",
30
31
  "better-sqlite3": "^9.4.3",
32
+ "chokidar": "^3.6.0",
31
33
  "commander": "^12.0.0",
32
34
  "configstore": "^6.0.0",
33
35
  "dax-sh": "^0.39.2",
@@ -41,13 +43,14 @@
41
43
  "minimist": "^1.2.8",
42
44
  "node-persist": "^4.0.1",
43
45
  "open": "^10.1.0",
44
- "perfect-cli": "1.0.12",
46
+ "perfect-cli": "1.0.13",
45
47
  "prompts": "^2.4.2",
46
48
  "react": "^18.2.0",
47
49
  "zod": "latest"
48
50
  },
49
51
  "devDependencies": {
50
52
  "@types/bun": "^1.0.8",
53
+ "@types/chokidar": "^2.1.3",
51
54
  "@types/configstore": "^6.0.2",
52
55
  "@types/lodash": "^4.14.202",
53
56
  "@types/mime-types": "^2.1.4",
@@ -60,4 +63,4 @@
60
63
  "tsx": "^4.7.1",
61
64
  "typescript": "^5.3.3"
62
65
  }
63
- }
66
+ }
@@ -1,5 +1,5 @@
1
1
  import "@tscircuit/react-fiber"
2
2
 
3
3
  export const MyCircuit = () => (
4
- <resistor name="R1" resistance="10kohm" footprint="0805" />
4
+ <resistor name="R2" resistance="20kohm" footprint="0805" />
5
5
  )
@@ -0,0 +1,9 @@
1
+ import { test, expect, describe } from "bun:test"
2
+ import { $ } from "bun"
3
+
4
+ test.skip("tsck init", async () => {
5
+ await $`rm -rf ./tests/example-init`
6
+ console.log(
7
+ await $`bun cli.ts init --name init-test --dir ./tests/example-init`.text()
8
+ )
9
+ })