spry.vala 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. using GLib;
  2. namespace Spry.Cli {
  3. /**
  4. * `spry` — the Spry application CLI.
  5. *
  6. * Scaffolds Statum + Spry applications (`spry new`), grows them with
  7. * app-owned page/action/resource/auth templates (`spry add …`), maintains
  8. * the static key material in `web-config.json` (`spry keys`), and
  9. * generates/builds the multi-stage Docker image (`spry docker`). Edits to
  10. * generated projects happen only within `// spry:*-begin/end` markers in
  11. * `src/main.vala` and `# spry:*-begin/end` markers in `meson.build`, so
  12. * `add` commands are idempotent and never reformat surrounding user code.
  13. */
  14. public class SpryCli : Object {
  15. private static string? tools_dir = null;
  16. private static string? libdir = null;
  17. private static bool force = false;
  18. private static string? route = null;
  19. private static string? out_path = null;
  20. private static string? tag = null;
  21. private static bool no_build = false;
  22. private static string? stack_dir = null;
  23. private static int dev_port = 8080;
  24. private static bool dev_fresh = false;
  25. private static bool dev_no_run = false;
  26. private const OptionEntry[] global_options = {
  27. { "tools-dir", '\0', 0, OptionArg.STRING, ref tools_dir, "Directory containing the statum-* tools (default: search PATH, then ~/.local/bin)", "DIR" },
  28. { "libdir", '\0', 0, OptionArg.STRING, ref libdir, "Library directory prepended to spawned tools' LD_LIBRARY_PATH/PKG_CONFIG_PATH (default: pkg-config spry-0.2 libdir)", "DIR" },
  29. { null }
  30. };
  31. private const OptionEntry[] new_options = {
  32. { "force", 'f', 0, OptionArg.NONE, ref force, "Overwrite generated files when the target directory already exists", null },
  33. { null }
  34. };
  35. private const OptionEntry[] page_options = {
  36. { "route", 'r', 0, OptionArg.STRING, ref route, "Page route (default: /<name>)", "PATH" },
  37. { null }
  38. };
  39. private const OptionEntry[] keys_options = {
  40. { "out", 'o', 0, OptionArg.FILENAME, ref out_path, "Configuration file to merge into (default: web-config.json)", "FILE" },
  41. { null }
  42. };
  43. private const OptionEntry[] docker_options = {
  44. { "tag", 't', 0, OptionArg.STRING, ref tag, "Image tag (default: the application name)", "TAG" },
  45. { "no-build", '\0', 0, OptionArg.NONE, ref no_build, "Only generate Dockerfile/.dockerignore; do not run the image build", null },
  46. { "stack-dir", '\0', 0, OptionArg.FILENAME, ref stack_dir, "Web-Stack checkout to COPY Statum/Spry from (default: the app's sibling directory)", "DIR" },
  47. { null }
  48. };
  49. private const OptionEntry[] dev_options = {
  50. { "port", 'p', 0, OptionArg.INT, ref dev_port, "Port to run the application on (default: 8080)", "N" },
  51. { "fresh", '\0', 0, OptionArg.NONE, ref dev_fresh, "Delete the SQLite database before starting", null },
  52. { "no-run", '\0', 0, OptionArg.NONE, ref dev_no_run, "Build once and exit; do not run or watch", null },
  53. { null }
  54. };
  55. private const string MESON_PAGES_BEGIN = "# spry:pages-begin";
  56. private const string MESON_PAGES_END = "# spry:pages-end";
  57. private const string MESON_RESOURCES_BEGIN = "# spry:resources-begin";
  58. private const string MESON_RESOURCES_END = "# spry:resources-end";
  59. private const string MESON_SOURCES_BEGIN = "# spry:sources-begin";
  60. private const string MESON_SOURCES_END = "# spry:sources-end";
  61. private const string MESON_GENERATED_BEGIN = "# spry:generated-begin";
  62. private const string MESON_GENERATED_END = "# spry:generated-end";
  63. private const string VALA_PAGES_BEGIN = "// spry:pages-begin";
  64. private const string VALA_PAGES_END = "// spry:pages-end";
  65. private const string VALA_ACTIONS_BEGIN = "// spry:actions-begin";
  66. private const string VALA_ACTIONS_END = "// spry:actions-end";
  67. private const string VALA_RESOURCES_BEGIN = "// spry:resources-begin";
  68. private const string VALA_RESOURCES_END = "// spry:resources-end";
  69. private const string VALA_AUTH_BEGIN = "// spry:auth-begin";
  70. private const string VALA_AUTH_END = "// spry:auth-end";
  71. private const string NAV_BEGIN = "<!-- spry:nav-begin -->";
  72. private const string NAV_END = "<!-- spry:nav-end -->";
  73. private const string NAV_AUTH_BEGIN = "<!-- spry:nav-auth-begin -->";
  74. private const string NAV_AUTH_END = "<!-- spry:nav-auth-end -->";
  75. public static int main(string[] args) {
  76. if (args.length < 2) {
  77. print_usage();
  78. return 1;
  79. }
  80. var command = args[1];
  81. if (command == "--help" || command == "-h" || command == "help") {
  82. print_usage();
  83. return 0;
  84. }
  85. try {
  86. switch (command) {
  87. case "new":
  88. return cmd_new(slice(args, 1));
  89. case "add":
  90. return cmd_add(slice(args, 1));
  91. case "keys":
  92. return cmd_keys(slice(args, 1));
  93. case "docker":
  94. return cmd_docker(slice(args, 1));
  95. case "dev":
  96. return cmd_dev(slice(args, 1));
  97. default:
  98. stderr.printf("Error: Unknown command '%s'.\n", command);
  99. print_usage();
  100. return 1;
  101. }
  102. } catch (OptionError e) {
  103. stderr.printf("Error: %s\n", e.message);
  104. return 1;
  105. } catch (Error e) {
  106. stderr.printf("Error: %s\n", e.message);
  107. return 1;
  108. }
  109. }
  110. private static void print_usage() {
  111. stdout.printf(
  112. "Usage:\n" +
  113. " spry <command> [options]\n" +
  114. "\n" +
  115. "Commands:\n" +
  116. " new <name> Scaffold a new Statum + Spry application\n" +
  117. " add page <name> [--route PATH] Add a page (HTML + entrypoint + wiring)\n" +
  118. " add action <name> Add a StatumAction skeleton\n" +
  119. " add resource <file> Embed a static resource (statum-mkres)\n" +
  120. " add login Add the login page (Spry.Actions.LoginAction)\n" +
  121. " add register Add the registration page\n" +
  122. " add user-management Add the admin user-management page\n" +
  123. " keys [--out FILE] Generate/merge static keys into web-config.json\n" +
  124. " docker [--tag T] [--no-build] Generate (and optionally build) a Dockerfile\n" +
  125. " dev [--port N] [--fresh] Build, run and watch: rebuild + restart on save\n" +
  126. "\n" +
  127. "Options:\n" +
  128. " --tools-dir DIR Directory containing the statum-* tools (default: PATH)\n" +
  129. " --libdir DIR Library dir for spawned tools' LD_LIBRARY_PATH\n" +
  130. " (default: pkg-config spry-0.2 libdir)\n" +
  131. " --help Show this help\n");
  132. }
  133. private static void print_add_usage() {
  134. stdout.printf(
  135. "Usage:\n" +
  136. " spry add page <name> [--route /path]\n" +
  137. " spry add action <name>\n" +
  138. " spry add resource <file>\n" +
  139. " spry add login\n" +
  140. " spry add register\n" +
  141. " spry add user-management\n");
  142. }
  143. // ----------------------------------------------------------------------
  144. // spry new
  145. // ----------------------------------------------------------------------
  146. private static int cmd_new(string[] args) throws Error, OptionError {
  147. parse(args, "<name> - scaffold a new Statum + Spry application", new_options);
  148. if (args.length < 2) {
  149. stderr.printf("Error: No application name specified.\n");
  150. return 1;
  151. }
  152. var name = args[1].down();
  153. if (!Generator.valid_name(name)) {
  154. stderr.printf("Error: '%s' is not a valid application name (letters, digits, '-' and '_', starting with a letter).\n", name);
  155. return 1;
  156. }
  157. var app_dir = Path.build_filename(Environment.get_current_dir(), name);
  158. if (FileUtils.test(app_dir, FileTest.EXISTS) && !force) {
  159. stderr.printf("Error: %s already exists (use --force to overwrite its generated files).\n", app_dir);
  160. return 1;
  161. }
  162. var vars = base_vars(name);
  163. write_app_file(app_dir, "meson.build", Generator.fill(Templates.PROJECT_MESON, vars));
  164. write_app_file(app_dir, "src/main.vala", Generator.fill(Templates.APP_MAIN, vars));
  165. write_app_file(app_dir, "src/pages/main.html", Generator.fill(Templates.LAYOUT, vars));
  166. write_app_file(app_dir, "src/pages/home.html", Generator.fill(Templates.HOME, vars));
  167. write_app_file(app_dir, "src/entrypoints/HomeEntrypoint.vala", Generator.fill(Templates.HOME_ENTRYPOINT, vars));
  168. write_app_file(app_dir, ".gitignore", Generator.fill(Templates.GITIGNORE, vars));
  169. write_app_file(app_dir, "README.md", Generator.fill(Templates.README, vars));
  170. Docker.generate(app_dir, name);
  171. var config_path = Path.build_filename(app_dir, "web-config.json");
  172. try {
  173. Keys.run(config_path, tools_dir, libdir);
  174. } catch (Error e) {
  175. stderr.printf("Warning: %s — wrote an empty config; run `spry keys` later.\n", e.message);
  176. Generator.write_file(config_path, "{\n \"statum\": {\n }\n}\n");
  177. }
  178. stdout.printf("Created %s\n", app_dir);
  179. stdout.printf("Next:\n cd %s\n spry dev\n", name);
  180. return 0;
  181. }
  182. // ----------------------------------------------------------------------
  183. // spry add …
  184. // ----------------------------------------------------------------------
  185. private static int cmd_add(string[] args) throws Error, OptionError {
  186. if (args.length < 2) {
  187. print_add_usage();
  188. return 1;
  189. }
  190. var what = args[1];
  191. var rest = slice(args, 1);
  192. switch (what) {
  193. case "page":
  194. return cmd_add_page(rest);
  195. case "action":
  196. return cmd_add_action(rest);
  197. case "resource":
  198. return cmd_add_resource(rest);
  199. case "login":
  200. return cmd_add_login(rest);
  201. case "register":
  202. return cmd_add_register(rest);
  203. case "user-management":
  204. return cmd_add_user_management(rest);
  205. default:
  206. stderr.printf("Error: Unknown add command '%s'.\n", what);
  207. print_add_usage();
  208. return 1;
  209. }
  210. }
  211. private static int cmd_add_page(string[] args) throws Error, OptionError {
  212. parse(args, "<name> - add a page (HTML + entrypoint + wiring)", page_options);
  213. if (args.length < 2) {
  214. stderr.printf("Error: No page name specified.\n");
  215. return 1;
  216. }
  217. var name = normalise(args[1]);
  218. if (!Generator.valid_name(name)) {
  219. stderr.printf("Error: '%s' is not a valid page name.\n", name);
  220. return 1;
  221. }
  222. var route_path = route != null ? (!)route : "/" + name;
  223. if (!route_path.has_prefix("/")) {
  224. route_path = "/" + route_path;
  225. }
  226. var app = require_app();
  227. add_page(app, name, Generator.pascal_case(name), route_path,
  228. Generator.fill(Templates.PAGE, page_vars(app, name, route_path)),
  229. Generator.fill(Templates.PAGE_ENTRYPOINT, page_vars(app, name, route_path)));
  230. stdout.printf("Added page %s at %s\n", name, route_path);
  231. return 0;
  232. }
  233. private static int cmd_add_action(string[] args) throws Error, OptionError {
  234. parse(args, "<name> - add a StatumAction skeleton", null);
  235. if (args.length < 2) {
  236. stderr.printf("Error: No action name specified.\n");
  237. return 1;
  238. }
  239. var name = normalise(args[1]);
  240. if (!Generator.valid_name(name)) {
  241. stderr.printf("Error: '%s' is not a valid action name.\n", name);
  242. return 1;
  243. }
  244. var cls = Generator.pascal_case(name);
  245. var app = require_app();
  246. var vars = app.vars;
  247. vars.set("CLASS", cls);
  248. write_new_app_file(app.dir, "src/actions/%sAction.vala".printf(cls),
  249. Generator.fill(Templates.ACTION, vars));
  250. edit_meson(app, "'src/actions/%sAction.vala',".printf(cls), MESON_SOURCES_BEGIN, MESON_SOURCES_END);
  251. edit_main(app, "statum.action<%sAction>();".printf(cls), VALA_ACTIONS_BEGIN, VALA_ACTIONS_END);
  252. stdout.printf("Added action %s\n", cls);
  253. return 0;
  254. }
  255. private static int cmd_add_resource(string[] args) throws Error, OptionError {
  256. parse(args, "<file> - embed a static resource", null);
  257. if (args.length < 2) {
  258. stderr.printf("Error: No resource file specified.\n");
  259. return 1;
  260. }
  261. var source = args[1];
  262. if (!FileUtils.test(source, FileTest.EXISTS)) {
  263. stderr.printf("Error: %s does not exist.\n", source);
  264. return 1;
  265. }
  266. var file_name = Path.get_basename(source);
  267. var stem = file_name.contains(".") ? file_name.substring(0, file_name.last_index_of(".")) : file_name;
  268. var cls = Generator.pascal_case(stem) + "Resource";
  269. var var_name = Generator.snake_case(stem) + "_resource";
  270. var content_type = content_type_for(file_name);
  271. var app = require_app();
  272. var resource_path = Path.build_filename(app.dir, "src", "resources", file_name);
  273. if (!FileUtils.test(resource_path, FileTest.EXISTS)) {
  274. Generator.write_file(resource_path, Generator.read_file(source));
  275. }
  276. var command = "[statum_mkres, '-o', '@OUTPUT@', '-n', '%s', '--class-name', '%s'".printf(file_name, cls);
  277. if (content_type != null) {
  278. command += ", '-c', '%s'".printf((!)content_type);
  279. }
  280. command += ", '@INPUT@']";
  281. var target = "%s = custom_target('%s-resource',\n".printf(var_name, Generator.snake_case(stem))
  282. + " input: 'src/resources/%s',\n".printf(file_name)
  283. + " output: '%s.vala',\n".printf(cls)
  284. + " command: %s\n".printf(command)
  285. + ")";
  286. edit_meson(app, target, MESON_RESOURCES_BEGIN, MESON_RESOURCES_END);
  287. edit_meson(app, "%s,".printf(var_name), MESON_GENERATED_BEGIN, MESON_GENERATED_END);
  288. edit_main(app, "statum.add_resource<%s>();".printf(cls), VALA_RESOURCES_BEGIN, VALA_RESOURCES_END);
  289. stdout.printf("Added resource %s as %s\n", source, cls);
  290. return 0;
  291. }
  292. private static int cmd_add_login(string[] args) throws Error, OptionError {
  293. parse(args, "- add the login page", null);
  294. var app = require_app();
  295. add_page(app, "login", "Login", "/login",
  296. Generator.fill(Templates.LOGIN, app.vars),
  297. Generator.fill(Templates.LOGIN_ENTRYPOINT, app.vars));
  298. // Keep Spry.Actions registrations in sync with
  299. // SpryAuth.register_actions (Spry/src/Auth.vala), the canonical
  300. // list of shipped actions.
  301. edit_main(app, "statum.action<Spry.Actions.LoginAction>();", VALA_AUTH_BEGIN, VALA_AUTH_END);
  302. edit_main(app, "statum.action<Spry.Actions.LogoutAction>();", VALA_AUTH_BEGIN, VALA_AUTH_END);
  303. edit_layout(app, "<a href=\"/login\" stm-if=\"!auth\">Login</a>", NAV_BEGIN, NAV_END);
  304. edit_layout(app, "<button type=\"button\" stm-action=\"auth.logout\">Log out</button>", NAV_AUTH_BEGIN, NAV_AUTH_END);
  305. Generator.append_section(Path.build_filename(app.dir, "README.md"),
  306. "## Guarding pages", Generator.fill(Templates.LOGIN_README, app.vars));
  307. stdout.printf("Added login page at /login\n");
  308. return 0;
  309. }
  310. private static int cmd_add_register(string[] args) throws Error, OptionError {
  311. parse(args, "- add the registration page", null);
  312. var app = require_app();
  313. add_page(app, "register", "Register", "/register",
  314. Generator.fill(Templates.REGISTER, app.vars),
  315. Generator.fill(Templates.REGISTER_ENTRYPOINT, app.vars));
  316. edit_main(app, "statum.action<Spry.Actions.RegisterAction>();", VALA_AUTH_BEGIN, VALA_AUTH_END);
  317. edit_layout(app, "<a href=\"/register\" stm-if=\"!auth\">Register</a>", NAV_BEGIN, NAV_END);
  318. stdout.printf("Added register page at /register\n");
  319. return 0;
  320. }
  321. private static int cmd_add_user_management(string[] args) throws Error, OptionError {
  322. parse(args, "- add the admin user-management page", null);
  323. var app = require_app();
  324. add_page(app, "user-management", "UserManagement", "/users",
  325. Generator.fill(Templates.USER_MANAGEMENT, app.vars),
  326. Generator.fill(Templates.USER_MANAGEMENT_ENTRYPOINT, app.vars));
  327. edit_main(app, "statum.action<Spry.Actions.SetUserEnabledAction>();", VALA_AUTH_BEGIN, VALA_AUTH_END);
  328. edit_main(app, "statum.action<Spry.Actions.GrantPermissionAction>();", VALA_AUTH_BEGIN, VALA_AUTH_END);
  329. edit_main(app, "statum.action<Spry.Actions.RevokePermissionAction>();", VALA_AUTH_BEGIN, VALA_AUTH_END);
  330. edit_main(app, "statum.action<Spry.Actions.AlterUserAction>();", VALA_AUTH_BEGIN, VALA_AUTH_END);
  331. edit_main(app, "statum.action<Spry.Actions.DeleteUserAction>();", VALA_AUTH_BEGIN, VALA_AUTH_END);
  332. edit_layout(app, "<a href=\"/users\" stm-if=\"auth && (auth.permissions.indexOf('admin') !== -1 || auth.permissions.indexOf('*') !== -1)\">Users</a>", NAV_BEGIN, NAV_END);
  333. stdout.printf("Added user-management page at /users\n");
  334. return 0;
  335. }
  336. // ----------------------------------------------------------------------
  337. // spry dev
  338. // ----------------------------------------------------------------------
  339. private static int cmd_dev(string[] args) throws Error, OptionError {
  340. parse(args, "- build, run and watch the application", dev_options);
  341. var app = require_app();
  342. return Dev.run(app.dir, app.name, dev_port, dev_fresh, dev_no_run, libdir);
  343. }
  344. // ----------------------------------------------------------------------
  345. // spry keys / spry docker
  346. // ----------------------------------------------------------------------
  347. private static int cmd_keys(string[] args) throws Error, OptionError {
  348. parse(args, "- generate and merge static keys into web-config.json", keys_options);
  349. Keys.run(out_path ?? "web-config.json", tools_dir, libdir);
  350. return 0;
  351. }
  352. private static int cmd_docker(string[] args) throws Error, OptionError {
  353. parse(args, "- generate (and optionally build) the Dockerfile", docker_options);
  354. var app = require_app();
  355. Docker.generate(app.dir, app.name);
  356. if (no_build) {
  357. return 0;
  358. }
  359. var stack = stack_dir ?? Path.get_dirname(app.dir);
  360. Docker.build(app.dir, app.name, tag ?? app.name, stack);
  361. return 0;
  362. }
  363. // ----------------------------------------------------------------------
  364. // Shared helpers
  365. // ----------------------------------------------------------------------
  366. /**
  367. * The surrounding spry application: the current directory, the app
  368. * name (directory basename) and the namespace its generated pages
  369. * use (read back from meson.build, falling back to the directory
  370. * name).
  371. */
  372. private class AppContext {
  373. public string dir;
  374. public string name;
  375. public string ns;
  376. public HashTable<string, string> vars;
  377. public AppContext(string dir, string name, string ns) {
  378. this.dir = dir;
  379. this.name = name;
  380. this.ns = ns;
  381. this.vars = new HashTable<string, string>(str_hash, str_equal);
  382. this.vars.insert("APP_NAME", name);
  383. this.vars.insert("NS", ns);
  384. }
  385. }
  386. private static AppContext require_app() throws Error {
  387. var cwd = Environment.get_current_dir();
  388. if (!FileUtils.test(Path.build_filename(cwd, "meson.build"), FileTest.EXISTS)
  389. || !FileUtils.test(Path.build_filename(cwd, "src", "main.vala"), FileTest.EXISTS)) {
  390. throw Generator.cli_error("not a spry application directory (run from the project root)");
  391. }
  392. var name = Path.get_basename(cwd);
  393. var ns = detect_ns(Generator.read_file(Path.build_filename(cwd, "meson.build")));
  394. if (ns.length == 0) {
  395. ns = Generator.pascal_case(name);
  396. }
  397. return new AppContext(cwd, name, ns);
  398. }
  399. private static string detect_ns(string meson) {
  400. var marker = "'--ns', '";
  401. var index = meson.index_of(marker);
  402. if (index < 0) {
  403. return "";
  404. }
  405. var start = index + marker.length;
  406. var stop = meson.index_of("'", start);
  407. return stop > start ? meson.substring(start, stop - start) : "";
  408. }
  409. private static HashTable<string, string> base_vars(string name) {
  410. var vars = new HashTable<string, string>(str_hash, str_equal);
  411. vars.insert("APP_NAME", name);
  412. vars.insert("NS", Generator.pascal_case(name));
  413. return vars;
  414. }
  415. private static HashTable<string, string> page_vars(AppContext app, string name, string route_path) {
  416. app.vars.set("CLASS", Generator.pascal_case(name));
  417. app.vars.set("ROUTE", route_path);
  418. return app.vars;
  419. }
  420. private static void add_page(AppContext app, string name, string cls, string route_path,
  421. string html, string entrypoint) throws Error {
  422. write_new_app_file(app.dir, "src/pages/%s.html".printf(name), html);
  423. write_new_app_file(app.dir, "src/entrypoints/%sEntrypoint.vala".printf(cls), entrypoint);
  424. var var_name = Generator.snake_case(name) + "_page";
  425. var target = "%s = custom_target('%s-page',\n".printf(var_name, Generator.snake_case(name))
  426. + " input: 'src/pages/%s.html',\n".printf(name)
  427. + " output: '%sPage.vala',\n".printf(cls)
  428. + " command: [statum_mkpstm, '-o', '@OUTPUT@', '-n', '%sPage', '--ns', '%s', '@INPUT@'],\n".printf(cls, app.ns)
  429. + " depend_files: files('src/pages/main.html')\n"
  430. + ")";
  431. edit_meson(app, target, MESON_PAGES_BEGIN, MESON_PAGES_END);
  432. edit_meson(app, "'src/entrypoints/%sEntrypoint.vala',".printf(cls), MESON_SOURCES_BEGIN, MESON_SOURCES_END);
  433. edit_meson(app, "%s,".printf(var_name), MESON_GENERATED_BEGIN, MESON_GENERATED_END);
  434. edit_main(app, "statum.add_page<%sPage, %sEntrypoint>();".printf(cls, cls), VALA_PAGES_BEGIN, VALA_PAGES_END);
  435. }
  436. private static void edit_meson(AppContext app, string snippet, string begin, string end) throws Error {
  437. var path = Path.build_filename(app.dir, "meson.build");
  438. Generator.write_file(path, Generator.insert_marked(Generator.read_file(path), begin, end, snippet));
  439. }
  440. private static void edit_main(AppContext app, string snippet, string begin, string end) throws Error {
  441. var path = Path.build_filename(app.dir, "src", "main.vala");
  442. Generator.write_file(path, Generator.insert_marked(Generator.read_file(path), begin, end, snippet));
  443. }
  444. private static void edit_layout(AppContext app, string snippet, string begin, string end) throws Error {
  445. var path = Path.build_filename(app.dir, "src", "pages", "main.html");
  446. Generator.write_file(path, Generator.insert_marked(Generator.read_file(path), begin, end, snippet));
  447. }
  448. private static void write_app_file(string app_dir, string relative, string contents) throws Error {
  449. Generator.write_file(Path.build_filename(app_dir, relative), contents);
  450. }
  451. /**
  452. * Writes a scaffolded file only when it does not exist yet, so
  453. * re-running `add` commands never clobbers user edits.
  454. */
  455. private static void write_new_app_file(string app_dir, string relative, string contents) throws Error {
  456. var path = Path.build_filename(app_dir, relative);
  457. if (FileUtils.test(path, FileTest.EXISTS)) {
  458. stdout.printf("%s already exists — left untouched\n", path);
  459. return;
  460. }
  461. Generator.write_file(path, contents);
  462. }
  463. private static string normalise(string name) {
  464. return name.down();
  465. }
  466. private static string? content_type_for(string filename) {
  467. var extension = filename.contains(".") ? filename.substring(filename.last_index_of(".") + 1).down() : "";
  468. switch (extension) {
  469. case "css":
  470. return "text/css";
  471. case "js":
  472. case "mjs":
  473. return "application/javascript";
  474. case "png":
  475. return "image/png";
  476. case "svg":
  477. return "image/svg+xml";
  478. case "jpg":
  479. case "jpeg":
  480. return "image/jpeg";
  481. case "ico":
  482. return "image/x-icon";
  483. case "woff2":
  484. return "font/woff2";
  485. case "woff":
  486. return "font/woff";
  487. default:
  488. return null;
  489. }
  490. }
  491. private static void parse(string[] args, string summary, OptionEntry[]? extra) throws OptionError {
  492. var context = new OptionContext(summary);
  493. context.add_main_entries(global_options, null);
  494. if (extra != null) {
  495. context.add_main_entries((!)extra, null);
  496. }
  497. context.parse(ref args);
  498. }
  499. /** Drops `skip` positional words after `args[0]`, keeping `args[0]`. */
  500. private static string[] slice(string[] args, int skip) {
  501. var rest = new string[int.max(1, args.length - skip)];
  502. rest[0] = args[0];
  503. for (int i = skip + 1; i < args.length; i++) {
  504. rest[i - skip] = args[i];
  505. }
  506. return rest;
  507. }
  508. }
  509. }