@abcnews/aunty 17.0.0-next.4 → 17.0.0-next.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.
Files changed (90) hide show
  1. package/CONTRIBUTING.md +6 -22
  2. package/README.md +23 -0
  3. package/TEMPLATES.md +112 -0
  4. package/eslint.config.mjs +6 -1
  5. package/package.json +25 -10
  6. package/src/bin/aunty.js +72 -0
  7. package/src/bin/commander.ts +174 -0
  8. package/src/commands/build/index.ts +23 -0
  9. package/src/commands/create/index.ts +148 -0
  10. package/src/commands/create/types.ts +7 -0
  11. package/src/commands/deploy/index.ts +40 -32
  12. package/src/commands/migrate/index.ts +320 -0
  13. package/src/commands/release/index.ts +127 -0
  14. package/src/commands/release-check/git.ts +14 -2
  15. package/src/commands/release-check/index.ts +139 -35
  16. package/src/commands/serve/index.ts +44 -0
  17. package/src/lib/colours.json +12 -0
  18. package/src/lib/constants.ts +3 -0
  19. package/src/{commands/deploy → lib}/ftp.ts +49 -23
  20. package/src/lib/git.ts +93 -0
  21. package/src/lib/initHelpers.ts +208 -0
  22. package/src/lib/semver.ts +50 -0
  23. package/src/lib/terminal.ts +85 -9
  24. package/src/lib/util.ts +66 -3
  25. package/src/lib/vite.ts +179 -0
  26. package/src/types.ts +16 -0
  27. package/templates/index.ts +11 -0
  28. package/templates/svelte/base/README.md +26 -0
  29. package/templates/svelte/base/contents/.editorconfig +12 -0
  30. package/templates/svelte/base/contents/.prettierrc +7 -0
  31. package/templates/svelte/base/contents/.vscode/extensions.json +3 -0
  32. package/templates/svelte/base/contents/LICENSE +21 -0
  33. package/templates/svelte/base/contents/README.md +16 -0
  34. package/templates/svelte/base/contents/_gitignore +24 -0
  35. package/templates/svelte/base/contents/index.html +22 -0
  36. package/templates/svelte/base/contents/package-lock.json +1373 -0
  37. package/templates/svelte/base/contents/package.json +29 -0
  38. package/templates/svelte/base/contents/src/App.svelte +21 -0
  39. package/templates/svelte/base/contents/src/assets/favicon.svg +4 -0
  40. package/templates/svelte/base/contents/src/components/Worm/Worm.svelte +59 -0
  41. package/templates/svelte/base/contents/src/components/Worm/worm.svg +4 -0
  42. package/templates/svelte/base/contents/src/iframe.ts +15 -0
  43. package/templates/svelte/base/contents/src/index.ts +26 -0
  44. package/templates/svelte/base/contents/svelte.config.js +2 -0
  45. package/templates/svelte/base/contents/tsconfig.app.json +22 -0
  46. package/templates/svelte/base/contents/tsconfig.json +7 -0
  47. package/templates/svelte/base/contents/tsconfig.node.json +28 -0
  48. package/templates/svelte/base/contents/vite-env.d.ts +7 -0
  49. package/templates/svelte/base/contents/vite.config.ts +40 -0
  50. package/templates/svelte/base/init.ts +39 -0
  51. package/templates/svelte/index.ts +52 -0
  52. package/templates/svelte/patch-builder/contents/builder/index.html +39 -0
  53. package/templates/svelte/patch-builder/contents/src/components/Builder/Builder.svelte +15 -0
  54. package/templates/svelte/patch-builder/init.ts +43 -0
  55. package/templates/svelte/patch-js/README.md +16 -0
  56. package/templates/svelte/patch-js/init.ts +61 -0
  57. package/templates/svelte/patch-odyssey/init.ts +10 -0
  58. package/templates/svelte/patch-scrollyteller/README.md +6 -0
  59. package/templates/svelte/patch-scrollyteller/contents/index.html +35 -0
  60. package/templates/svelte/patch-scrollyteller/contents/src/App.svelte +37 -0
  61. package/templates/svelte/patch-scrollyteller/contents/src/index.ts +36 -0
  62. package/templates/svelte/patch-scrollyteller/contents/src/types.ts +2 -0
  63. package/templates/svelte/patch-scrollyteller/init.ts +20 -0
  64. package/templates/vanilla/base/contents/LICENSE +21 -0
  65. package/templates/vanilla/base/contents/_gitignore +24 -0
  66. package/templates/vanilla/base/contents/index.html +22 -0
  67. package/templates/vanilla/base/contents/package-lock.json +999 -0
  68. package/templates/vanilla/base/contents/package.json +25 -0
  69. package/templates/vanilla/base/contents/public/favicon.svg +1 -0
  70. package/templates/vanilla/base/contents/public/icons.svg +24 -0
  71. package/templates/vanilla/base/contents/src/assets/hero.png +0 -0
  72. package/templates/vanilla/base/contents/src/assets/javascript.svg +1 -0
  73. package/templates/vanilla/base/contents/src/assets/vite.svg +1 -0
  74. package/templates/vanilla/base/contents/src/iframe.ts +17 -0
  75. package/templates/vanilla/base/contents/src/index.ts +17 -0
  76. package/templates/vanilla/base/contents/src/style.css +8 -0
  77. package/templates/vanilla/base/contents/tsconfig.app.json +23 -0
  78. package/templates/vanilla/base/contents/tsconfig.json +7 -0
  79. package/templates/vanilla/base/contents/tsconfig.node.json +28 -0
  80. package/templates/vanilla/base/contents/vite-env.d.ts +7 -0
  81. package/templates/vanilla/base/contents/vite.config.js +38 -0
  82. package/templates/vanilla/base/init.ts +33 -0
  83. package/templates/vanilla/index.ts +34 -0
  84. package/templates/vanilla/patch-js/README.md +0 -0
  85. package/templates/vanilla/patch-js/init.ts +27 -0
  86. package/test/helpers.ts +198 -0
  87. package/test/semver.test.ts +45 -0
  88. package/test/templates-e2e.test.ts +268 -0
  89. package/tsconfig.json +1 -0
  90. package/src/bin/aunty.ts +0 -60
package/CONTRIBUTING.md CHANGED
@@ -20,35 +20,19 @@ To revert to your original global install, run:
20
20
  npm unlink
21
21
  ```
22
22
 
23
+ For ease of use, you can use the Aunty version directly with `node <path to aunty repo> -h`.
24
+
23
25
  ## Releasing new versions of `@abcnews/aunty`
24
26
 
25
- Releases are managed by `release-it`. To release a new version of aunty from the default branch, run:
27
+ Releases are managed by `np`. To release a new version of aunty from the default branch, run:
26
28
 
27
- ```
29
+ ```bash
28
30
  npm run release
29
31
  ```
30
32
 
31
- By default this will do the following:
32
-
33
- 1. Bump the `patch` version in `package.json` (and `package-lock.json` if it exists)
34
- 2. Commit and tag that version.
35
- 3. Push the tag & commit to GitHub
36
- 4. Publish to npm
37
-
38
- If you want to cut a minor or major release, run either of the following commands instead:
39
-
40
- ```
41
- npm run release -- minor
42
- npm run release -- major
43
- ```
44
-
45
- If you're ever unsure about what will happen, you can perform a dry run (which logs to the console) by running:
46
-
47
- ```
48
- npm run release -- --dry-run
49
- ```
33
+ This will run tests, prompt you to select the version increment, and then publish to npm, commit, tag, and push to GitHub.
50
34
 
51
- View the [`release-it` docs](https://www.npmjs.com/package/release-it) for full usage examples, including pre-release and npm tag management.
35
+ For more information, see the [`np` documentation](https://github.com/sindresorhus/np).
52
36
 
53
37
  ## Style
54
38
 
package/README.md CHANGED
@@ -3,3 +3,26 @@
3
3
  A toolkit for working with ABC News projects
4
4
 
5
5
  THIS IS A NEW BRANCH FOR AUNTY@NEXT PLEASE BITE OFF A CHUNK OF WORK AND MERGE INTO the `aunty-next` BRANCH
6
+
7
+ # Contributing
8
+
9
+ The repo is split up by commands. You should be able to jump into the command you're interested in, and follow the logic from start to finish.
10
+
11
+ ### Command Entry Points
12
+
13
+ - [build](./src/commands/build/index.ts)
14
+ - [create](./src/commands/create/index.ts) (aliased as `new`)
15
+ - [deploy](./src/commands/deploy/index.ts)
16
+ - [release](./src/commands/release/index.ts)
17
+ - [release-check](./src/commands/release-check/index.ts)
18
+ - [serve](./src/commands/serve/index.ts)
19
+
20
+ Aunty uses the pattern of "helpers" to abstract complex logic out and hopefully make our main entrypoints flat and readable.
21
+
22
+ The main CLI logic is in [src/bin/commander.ts](./src/bin/commander.ts).
23
+
24
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for specifics.
25
+
26
+ # Templates
27
+
28
+ `aunty create` uses a custom template system to create new projects. See [TEMPLATES.md](./TEMPLATES.md) for specifics.
package/TEMPLATES.md ADDED
@@ -0,0 +1,112 @@
1
+ `aunty create` uses a custom template system to create new projects. Templates have three main components:
2
+
3
+ 1. a programmatic index that uses clack prompts to ask for desired configuration
4
+ 2. a base template containing the dir to copy from + a programmatic script to perform the copy and any other tweaks
5
+ 3. a series of "patches" containing programmatic scripts to copy over additional files or make changes to the base template.
6
+
7
+ The intention is to make this followable in logic, so you don't need any prior knowledge to make a new template. Hopefully this is helpful.
8
+
9
+ ## Guide to making a new template
10
+
11
+ The following steps will guide you through creating a new template from scratch.
12
+
13
+ ### 1. Scaffold your starting point
14
+
15
+ It's usually easier to start with a working scaffold than writing a project framework from scratch.
16
+
17
+ 1. Generate your base project in a temporary folder using standard initialization tools (e.g., `npm create vite@latest`).
18
+ 2. Test the generated project to ensure it works as expected before converting it into a template.
19
+
20
+ ### 2. Create the Base Template
21
+
22
+ Now you need to bring those scaffolded files into the Aunty project.
23
+
24
+ 1. Create the directory structure for your template's base contents: `templates/<your-template>/base/contents`.
25
+ 2. Copy your scaffolded project files into this `contents/` directory.
26
+ 3. **Rename `.gitignore` to `_gitignore`**.
27
+
28
+ **Note:** NPM drops `.gitignore` files by default during package publishing. If you don't rename it, the file won't be included when Aunty is installed. Aunty's initialization helper will automatically rename it back to `.gitignore` when a user creates a new project.
29
+
30
+ ### 3. Create the Base Init Script
31
+
32
+ Next, you need a script to copy those base files to the user's destination directory.
33
+
34
+ 1. Create `templates/<your-template>/base/init.ts`.
35
+ 2. Export an `init(options: InitOptions)` function.
36
+ 3. Use the built-in helper functions (from [`src/lib/initHelpers.ts`](./src/lib/initHelpers.ts)) to copy the contents and install Aunty as a dependency.
37
+
38
+ ```typescript
39
+ import path from "node:path";
40
+ import { copyContents, installAunty } from "../../../src/lib/initHelpers.ts";
41
+ import type { InitOptions } from "../../../src/commands/create/types.ts";
42
+
43
+ export async function init(options: InitOptions) {
44
+ // Copy the files from base/contents to the new project directory
45
+ await copyContents(path.join(import.meta.dirname, "contents"), options.dir);
46
+
47
+ // Ensure @abcnews/aunty is added to the devDependencies
48
+ await installAunty(options.dir);
49
+ }
50
+ ```
51
+
52
+ ### 4. Create the Main Entry Point
53
+
54
+ The entry point handles the user flow, asking any required questions before calling the init script.
55
+
56
+ 1. Create `templates/<your-template>/index.ts`.
57
+ 2. Export a default `run(options: InitOptions)` function.
58
+ 3. Use [`@clack/prompts`](https://github.com/natemoo-re/clack/tree/main/packages/prompts) to gather user preferences (e.g., confirm if they want TypeScript, or select a variant).
59
+ 4. Invoke your base `init.ts` script.
60
+
61
+ ```typescript
62
+ import { confirm, isCancel, cancel } from "@clack/prompts";
63
+ import type { InitOptions } from "../../src/commands/create/types.ts";
64
+ import { init as baseInit } from "./base/init.ts";
65
+ import { init as jsInit } from "./patch-js/init.ts";
66
+
67
+ export default async function run(options: InitOptions) {
68
+ const useTypescript = await confirm({
69
+ message: "Do you want to use TypeScript?",
70
+ initialValue: true,
71
+ });
72
+
73
+ if (isCancel(useTypescript)) {
74
+ cancel("Operation cancelled.");
75
+ return 1;
76
+ }
77
+
78
+ // 1. Run the base setup first
79
+ await baseInit(options);
80
+
81
+ // 2. Apply patches conditionally based on user answers
82
+ if (!useTypescript) {
83
+ await jsInit(options);
84
+ }
85
+
86
+ return 0;
87
+ }
88
+ ```
89
+
90
+ ### 5. Create Patches (Optional)
91
+
92
+ If your template requires variants (e.g., an Odyssey version vs a standard version, or JS vs TS), you can create "patches".
93
+
94
+ 1. Create a patch directory (e.g., `templates/<your-template>/patch-js/contents` and `templates/<your-template>/patch-js/init.ts`).
95
+ 2. Patches are just smaller init scripts that layer extra files, string replacements, or `package.json` edits on top of the base template.
96
+ 3. Invoke the patch's `init` function in your main `index.ts` conditionally, as demonstrated in the snippet in Step 4.
97
+
98
+ ### 6. Test Locally
99
+
100
+ While developing your new template, you can test it locally by running the CLI from the root of the Aunty repository:
101
+
102
+ ```bash
103
+ node . create
104
+ ```
105
+
106
+ or, in another dir with
107
+
108
+ ```bash
109
+ node ../aunty create
110
+ ```
111
+
112
+ This will invoke your local version of Aunty, allowing you to test the prompt flow and verify the generated output in your chosen destination directory.
package/eslint.config.mjs CHANGED
@@ -3,5 +3,10 @@ import tseslint from 'typescript-eslint';
3
3
 
4
4
  export default [
5
5
  js.configs.recommended,
6
- ...tseslint.configs.recommended
6
+ ...tseslint.configs.recommended,
7
+ {
8
+ "env": {
9
+ "node": true,
10
+ }
11
+ }
7
12
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abcnews/aunty",
3
- "version": "17.0.0-next.4",
3
+ "version": "17.0.0-next.5",
4
4
  "type": "module",
5
5
  "description": "ABC News Digital developer toolkit",
6
6
  "repository": {
@@ -15,34 +15,43 @@
15
15
  "Ash Kyd <kyd.ashley@abc.net.au>"
16
16
  ],
17
17
  "scripts": {
18
- "build": "tsc",
18
+ "build": "esbuild src/bin/commander.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/bin/commander.js",
19
+ "prepublishOnly": "npm run build",
19
20
  "dev": "tsx src/bin/aunty.ts",
20
- "test": "echo \"Put your tests here\" && exit 1",
21
+ "test": "node --experimental-test-module-mocks --import tsx --test test/*.test.ts",
21
22
  "lint": "eslint src",
22
23
  "clean": "rimraf dist",
23
- "prebuild": "npm run clean"
24
+ "prebuild": "npm run clean",
25
+ "release": "np"
24
26
  },
25
27
  "publishConfig": {
26
28
  "access": "public"
27
29
  },
28
- "main": "src/bin/aunty.ts",
30
+ "main": "src/bin/aunty.js",
31
+ "exports": {
32
+ ".": "./src/bin/aunty.js",
33
+ "./vite": "./src/lib/vite.ts"
34
+ },
29
35
  "bin": {
30
- "aunty": "src/bin/aunty.ts",
31
- "nt": "src/bin/aunty.ts"
36
+ "aunty": "src/bin/aunty.js",
37
+ "nt": "src/bin/aunty.js"
32
38
  },
33
39
  "engines": {
34
- "node": ">=20"
40
+ "node": ">=24"
35
41
  },
36
42
  "devDependencies": {
37
43
  "@eslint/js": "^9.39.2",
38
44
  "@types/async": "^3.2.25",
39
45
  "@types/fs-extra": "^11.0.4",
46
+ "@types/lodash-es": "^4.17.12",
40
47
  "@types/node": "^25.0.10",
48
+ "@types/semver": "^7.7.1",
49
+ "esbuild": "^0.28.1",
41
50
  "eslint": "^9.39.2",
42
51
  "install": "^0.13.0",
52
+ "np": "^12.0.0",
43
53
  "npm": "^11.11.1",
44
54
  "rimraf": "^6.1.2",
45
- "tsx": "^4.21.0",
46
55
  "typescript": "^5.9.3",
47
56
  "typescript-eslint": "^8.54.0"
48
57
  },
@@ -51,10 +60,16 @@
51
60
  "async": "^3.2.6",
52
61
  "basic-ftp": "^5.2.0",
53
62
  "commander": "^14.0.3",
54
- "ora": "^9.3.0",
63
+ "lodash-es": "^4.17.23",
55
64
  "picocolors": "^1.1.1",
65
+ "semver": "^7.8.5",
56
66
  "slugify": "^1.6.8",
57
67
  "tsx": "^4.21.0",
58
68
  "zx": "^8.8.5"
69
+ },
70
+ "allowScripts": {
71
+ "esbuild@0.28.1": true,
72
+ "esbuild@0.27.7": true,
73
+ "fsevents@2.3.3": true
59
74
  }
60
75
  }
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @file Aunty CLI Entry Point
5
+ *
6
+ * This is the main file that launches Aunty. It handles:
7
+ * - Deferring to the locally installed version if available
8
+ * - Deciding whether to use the built JS in prod, or `tsx` in dev-like environments
9
+ *
10
+ * This file MUST be plain JavaScript to ensure compatibility as a CLI entry point.
11
+ */
12
+
13
+ import { existsSync } from "node:fs";
14
+ import { realpath } from "node:fs/promises";
15
+ import path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { $ } from "zx";
18
+
19
+ const filename = fileURLToPath(import.meta.url);
20
+ const dirname = path.dirname(filename);
21
+
22
+ /**
23
+ * Bootstrap the CLI by preferring a local version if it exists.
24
+ */
25
+ async function run() {
26
+ const localAuntyPath = path.join(
27
+ process.cwd(),
28
+ "node_modules/@abcnews/aunty/src/bin/aunty.js",
29
+ );
30
+
31
+ // If a local version exists and we are not it, prefer the local version.
32
+ // Do not defer to local version for the 'migrate' command, as it is used to upgrade the project.
33
+ const isMigrate = process.argv.includes("migrate");
34
+ if (
35
+ !isMigrate &&
36
+ existsSync(localAuntyPath) &&
37
+ (await realpath(localAuntyPath)) !== (await realpath(filename))
38
+ ) {
39
+ const result = await $({ stdio: "inherit", nothrow: true })`${process.execPath} ${localAuntyPath} ${process.argv.slice(2)}`;
40
+ process.exit(result.exitCode);
41
+ }
42
+
43
+ // Resolve paths for the built JS and original TS commander files.
44
+ const builtCommanderPath = path.resolve(dirname, "../../dist/bin/commander.js");
45
+ const commanderPath = path.resolve(dirname, "commander.ts");
46
+
47
+ let execPath;
48
+ let execArgs;
49
+
50
+ /** If running from node_modules, we are almost certainly running in prod */
51
+ const isRunningFromNodeModules = filename.includes("node_modules");
52
+
53
+ if (isRunningFromNodeModules && existsSync(builtCommanderPath)) {
54
+ // If the built files are available and we are executing from node_modules, use those directly with node
55
+ execPath = process.execPath;
56
+ execArgs = [builtCommanderPath, ...process.argv.slice(2)];
57
+ } else {
58
+ // Otherwise run the package via tsx
59
+ // This is hit for local dev or when running straight from a Github branch
60
+ const localTsx = path.resolve(dirname, "../../node_modules/.bin/tsx");
61
+ execPath = existsSync(localTsx) ? localTsx : "tsx";
62
+ execArgs = [commanderPath, ...process.argv.slice(2)];
63
+ }
64
+
65
+ const result = await $({ stdio: "inherit", nothrow: true })`${execPath} ${execArgs}`;
66
+ process.exit(result.exitCode);
67
+ }
68
+
69
+ run().catch((err) => {
70
+ console.error(err);
71
+ process.exit(1);
72
+ });
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @file
3
+ * Core CLI logic using Commander.js.
4
+ */
5
+
6
+ import path from "node:path";
7
+ import { Command, Help } from "commander";
8
+ import { run as runDeploy } from "../commands/deploy/index.ts";
9
+ import { run as runReleaseCheck } from "../commands/release-check/index.ts";
10
+ import { run as runRelease } from "../commands/release/index.ts";
11
+ import { run as runCreate } from "../commands/create/index.ts";
12
+ import { run as runBuild } from "../commands/build/index.ts";
13
+ import { run as runServe } from "../commands/serve/index.ts";
14
+ import { run as runMigrate } from "../commands/migrate/index.ts";
15
+ import { getHeader, printDevColours } from "../lib/terminal.ts";
16
+ import { loadJson } from "../lib/util.ts";
17
+ import pc from "picocolors";
18
+ import rawColours from "../lib/colours.json" with { type: "json" };
19
+
20
+ interface Colours {
21
+ commands: Record<string, string>;
22
+ }
23
+
24
+ const colours = rawColours as unknown as Colours;
25
+ const pcColors = pc as unknown as Record<string, (str: string) => string>;
26
+
27
+ const pkgPath = path.join(import.meta.dirname, "../../package.json");
28
+ const pkg = (await loadJson(pkgPath)) as {
29
+ version: string;
30
+ name: string;
31
+ description: string;
32
+ };
33
+ const version = pkg?.version || "0.0.0";
34
+
35
+ const program = new Command();
36
+
37
+ program
38
+ .name("aunty")
39
+ .description(
40
+ getHeader(
41
+ `${pkg.name}${pc.dim("@" + pkg.version)}`,
42
+ `${pc.dim(pkg.description.slice(0, 73))}`,
43
+ { prepend: "" },
44
+ ) +
45
+ "\n" +
46
+ pc.dim("─".repeat(80)),
47
+ )
48
+ .version(version);
49
+
50
+ /**
51
+ * Configures the CLI help output to colorise subcommand terms (e.g. 'deploy', 'create')
52
+ * in their designated project palette colors defined in colours.json.
53
+ */
54
+ program.configureHelp({
55
+ subcommandTerm: (cmd) => {
56
+ const name = cmd.name();
57
+ const colourName = colours.commands[name];
58
+ const colourFn =
59
+ colourName && typeof pcColors[colourName] === "function"
60
+ ? pcColors[colourName]
61
+ : (str: string) => str;
62
+ const term = new Help().subcommandTerm(cmd);
63
+ return term.replace(name, colourFn(name));
64
+ },
65
+ formatHelp: (cmd, helper) => {
66
+ const baseHelp = new Help().formatHelp(cmd, helper);
67
+ if (cmd.parent) {
68
+ const name = cmd.name();
69
+ const colourName = colours.commands[name] || "rainbow";
70
+ const logoHeader = getHeader("aunty", name, {
71
+ prepend: "",
72
+ colour: colourName,
73
+ });
74
+ return `${logoHeader}\n\n${baseHelp}`;
75
+ }
76
+ return baseHelp;
77
+ },
78
+ });
79
+
80
+ program
81
+ .command("deploy")
82
+ .description(
83
+ "Deploy the project to FTP. This deploys the current dist/ folder to the specified remote folder, falling back to the current version number. Use this to retry a failed release.",
84
+ )
85
+ .argument(
86
+ "[destDir]",
87
+ "Override the target folder name (defaults to package.json version)",
88
+ )
89
+ .option("-d, --dry-run", "Show what would happen without uploading", false)
90
+ .option("-f, --force", "Overwrite the remote directory if it exists", false)
91
+ .action(async (destDir, options) => {
92
+ process.exit(await runDeploy({ destDir, ...options }));
93
+ });
94
+
95
+ program
96
+ .command("create")
97
+ .description("Create a new project from a template")
98
+ .argument("[destDir]", "Directory to create the project in")
99
+ .action(async (destDir) => {
100
+ process.exit(await runCreate(destDir));
101
+ });
102
+
103
+ program
104
+ .command("new", { hidden: true })
105
+ .description("Alias for create")
106
+ .argument("[destDir]", "Directory to create the project in")
107
+ .action(async (destDir) => {
108
+ process.exit(await runCreate(destDir));
109
+ });
110
+
111
+ program
112
+ .command("release-check")
113
+ .description("Perform pre-release checks")
114
+ .action(async () => {
115
+ process.exit(await runReleaseCheck());
116
+ });
117
+
118
+ program
119
+ .command("release")
120
+ .description("Version and release your project to FTP")
121
+ .option("--version <version>", "Override the release version directly")
122
+ .action(async (options) => {
123
+ process.exit(await runRelease(options));
124
+ });
125
+
126
+ program
127
+ .command("serve")
128
+ .description("Start a local development server")
129
+ .action(async () => {
130
+ await runServe();
131
+ });
132
+
133
+ program
134
+ .command("build")
135
+ .description("Build the project for production")
136
+ .action(async () => {
137
+ process.exit(await runBuild());
138
+ });
139
+
140
+ program
141
+ .command("migrate")
142
+ .description("Migrate a legacy project to use Vite and the latest version of aunty")
143
+ .option("-y, --yes", "Skip confirmation prompt", false)
144
+ .option("--skip-git", "Skip Git safety checks", false)
145
+ .action(async (options) => {
146
+ process.exit(await runMigrate(options));
147
+ });
148
+
149
+ program
150
+ .command("dev-colours", { hidden: true })
151
+ .description("Test all available gradient colours")
152
+ .action(() => {
153
+ printDevColours();
154
+ process.exit(0);
155
+ });
156
+
157
+ try {
158
+ // Start Commander
159
+ await program.parseAsync(process.argv);
160
+ } catch (err: unknown) {
161
+ // If Commander throws, print our unhandled exception message
162
+ if (err instanceof Error) {
163
+ console.error(`${pc.dim(err.stack)}`);
164
+ } else {
165
+ console.error(`${pc.red(pc.bold("Error:"))} ${pc.red(`‘${String(err)}’`)}`);
166
+ }
167
+
168
+ console.error(
169
+ `${pc.red(pc.bold("■ Aunty has encountered an unhandled error."))}
170
+ └ This is likely a bug. Please report it at: ${pc.cyan("https://github.com/abcnews/aunty/issues/new")}`,
171
+ );
172
+
173
+ process.exit(1);
174
+ }
@@ -0,0 +1,23 @@
1
+ import { intro, outro, log } from "@clack/prompts";
2
+ import pc from "picocolors";
3
+ import { getHeader } from "../../lib/terminal.ts";
4
+ import { runBuild } from "../../lib/util.ts";
5
+
6
+ /**
7
+ * The main entry point for the 'aunty build' command.
8
+ */
9
+ export async function run(): Promise<number> {
10
+ intro(getHeader(pc.dim("aunty"), "build", { colour: "blue" }));
11
+ outro("Building project for production...");
12
+
13
+ try {
14
+ await runBuild({ stdio: "inherit" });
15
+ return 0;
16
+ } catch (err: any) {
17
+ if (err.exitCode !== undefined) {
18
+ return err.exitCode;
19
+ }
20
+ log.error(err.message || `Failed to start npm command: ${err.message}`);
21
+ return 1;
22
+ }
23
+ }
@@ -0,0 +1,148 @@
1
+ import {
2
+ intro,
3
+ outro,
4
+ text,
5
+ select,
6
+ confirm,
7
+ log,
8
+ cancel,
9
+ isCancel,
10
+ } from "@clack/prompts";
11
+ import path from "node:path";
12
+ import fs from "node:fs/promises";
13
+ import pc from "picocolors";
14
+ import { $ } from "zx";
15
+ import { getHeader, spin } from "../../lib/terminal.ts";
16
+ import { isProjectNameAndVersionAvailable } from "../../lib/ftp.ts";
17
+ import templates from "../../../templates/index.ts";
18
+
19
+ /**
20
+ * The main entry point for the 'aunty create' command.
21
+ */
22
+ export async function run(destDirArg?: string): Promise<number> {
23
+ intro(getHeader(pc.dim("aunty"), "create", { colour: "green" }));
24
+
25
+ // 1. Get project name
26
+ const projectName =
27
+ destDirArg ||
28
+ (await text({
29
+ message: "What is your project named?",
30
+ placeholder: destDirArg || "my-new-project",
31
+ validate: (value) => {
32
+ if (!value || value.length === 0) {
33
+ return "Project name is required";
34
+ }
35
+ if (!value.match(/^[a-z0-9-]+$/)) {
36
+ return "Project name should be lowercase-alphanumeric-with-hypens";
37
+ }
38
+ },
39
+ }));
40
+
41
+ if (isCancel(projectName)) {
42
+ cancel("Operation cancelled.");
43
+ return 0;
44
+ }
45
+
46
+ const finalProjectName = projectName || destDirArg || "my-new-project";
47
+ const projectDest = path.resolve(process.cwd(), finalProjectName);
48
+
49
+ // 1.5. Check FTP for name collision
50
+ const ftpSpinner = spin("Checking project name availability...");
51
+ const isAvailable = await isProjectNameAndVersionAvailable(finalProjectName);
52
+ ftpSpinner.stop("Project name checked");
53
+
54
+ if (isAvailable === "error") {
55
+ const shouldContinue = await confirm({
56
+ message: pc.red(
57
+ "Could not connect to FTP to check name availability. Continue anyway?",
58
+ ),
59
+ initialValue: true,
60
+ });
61
+ if (!shouldContinue || isCancel(shouldContinue)) {
62
+ cancel("Create cancelled.");
63
+ return 0;
64
+ }
65
+ }
66
+ if (isAvailable === "exists") {
67
+ const shouldContinue = await confirm({
68
+ message: pc.yellow(
69
+ `Project name "${finalProjectName}" already exists on FTP. Continue?`,
70
+ ),
71
+ initialValue: false,
72
+ });
73
+ if (!shouldContinue || isCancel(shouldContinue)) {
74
+ cancel("Create cancelled.");
75
+ return 0;
76
+ }
77
+ }
78
+
79
+ // 2. Select project type
80
+ const projectType = await select({
81
+ message: "Select a project type:",
82
+ options: templates.projects.map((p) => ({ value: p, label: p.name })),
83
+ });
84
+
85
+ if (isCancel(projectType)) {
86
+ cancel("Operation cancelled.");
87
+ return 0;
88
+ }
89
+
90
+ const selectedProject = projectType;
91
+
92
+ // 3. Create directory
93
+ if (
94
+ await fs
95
+ .access(projectDest)
96
+ .then(() => true)
97
+ .catch(() => false)
98
+ ) {
99
+ cancel(`Directory ${pc.cyan(projectDest)} already exists.`);
100
+ return 1;
101
+ }
102
+
103
+ // 4. Run initialization
104
+ try {
105
+ const exitCode = await selectedProject.run({
106
+ projectName: finalProjectName,
107
+ baseDir: projectDest,
108
+ });
109
+
110
+ if (exitCode !== 0) {
111
+ return 0;
112
+ }
113
+ } catch (err: unknown) {
114
+ const message = err instanceof Error ? err.message : String(err);
115
+ cancel(`Failed to create project: ${message}`);
116
+ if (err instanceof Error && err.stack) {
117
+ console.error(err.stack);
118
+ }
119
+ return 1;
120
+ }
121
+
122
+ // 7. NPM Install
123
+ const installSpinner = spin("Installing dependencies...");
124
+ try {
125
+ await $({ cwd: projectDest })`npm install`.quiet();
126
+ installSpinner.stop("Dependencies installed");
127
+ } catch {
128
+ installSpinner.cancel(
129
+ "Failed to install dependencies. You may need to run 'npm install' manually.",
130
+ );
131
+ }
132
+
133
+ // 8. Initialize Git Repository
134
+ try {
135
+ await $({ cwd: projectDest })`git init`.quiet();
136
+ } catch {}
137
+
138
+ log.step("Next steps:");
139
+ log.message(
140
+ `
141
+ ${pc.bold(1)}. cd ${finalProjectName}
142
+ ${pc.bold(2)}. npm run dev
143
+ `.trim(),
144
+ );
145
+
146
+ outro("Happy coding.");
147
+ return 0;
148
+ }