@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
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Options passed to a template's init function.
3
+ */
4
+ export interface InitOptions {
5
+ projectName: string;
6
+ baseDir: string;
7
+ }
@@ -1,11 +1,15 @@
1
- import { intro, outro, confirm, log, cancel } from "@clack/prompts";
1
+ import { intro, outro, confirm, log, cancel, isCancel } from "@clack/prompts";
2
2
  import path from "node:path";
3
3
  import pc from "picocolors";
4
- import { FtpClient } from "./ftp.ts";
5
- import { loadJson, formatSize } from "../../lib/util.ts";
4
+ import { FtpClient } from "../../lib/ftp.ts";
5
+ import { formatSize, findProjectDetails } from "../../lib/util.ts";
6
6
  import { getHeader, spin } from "../../lib/terminal.ts";
7
+ import {
8
+ BUILD_DIRECTORY_NAME,
9
+ FTP_PROJECTS_PATH,
10
+ PUBLIC_PROJECTS_URL,
11
+ } from "../../lib/constants.ts";
7
12
  import { getFileInventory } from "./fs.ts";
8
- import { BUILD_DIRECTORY_NAME } from "../../lib/constants.ts";
9
13
  import slugify from "slugify";
10
14
 
11
15
  interface DeployOptions {
@@ -13,32 +17,29 @@ interface DeployOptions {
13
17
  buildDir?: string;
14
18
  dryRun?: boolean;
15
19
  force?: boolean;
20
+ skipHeader?: boolean;
16
21
  }
17
22
 
18
23
  /**
19
24
  * The main entry point for the 'aunty deploy' command.
20
25
  */
21
26
  export async function run(options: DeployOptions = {}): Promise<number> {
22
- const projectRoot = process.cwd();
23
-
24
- intro(
25
- getHeader(
26
- pc.dim("aunty"),
27
- `deploy${options.dryRun ? ` ${pc.cyan("[dry]")}` : ""}`,
28
- ),
29
- );
27
+ if (!options.skipHeader) {
28
+ intro(
29
+ getHeader(
30
+ pc.dim("aunty"),
31
+ `deploy${options.dryRun ? ` ${pc.cyan("[dry]")}` : ""}`,
32
+ { colour: "red" },
33
+ ),
34
+ );
35
+ }
30
36
 
31
37
  // 1. Load config
32
- const config = (await loadJson(path.join(projectRoot, "package.json"))) as {
33
- name: string;
34
- version: string;
35
- } | null;
38
+ const details = await findProjectDetails(process.cwd());
36
39
 
37
- if (!config) {
38
- log.error(`package.json not found in ${projectRoot}`);
39
- return 1;
40
- }
40
+ if (!details) return 1;
41
41
 
42
+ const { root: projectRoot, pkg: config } = details;
42
43
  const { name, version } = config;
43
44
  if (!name || !version) {
44
45
  cancel("Missing name or version in package.json");
@@ -50,17 +51,19 @@ export async function run(options: DeployOptions = {}): Promise<number> {
50
51
  projectRoot,
51
52
  options.buildDir || BUILD_DIRECTORY_NAME,
52
53
  );
53
- const nameSlug = (slugify as any)(name, { strict: true });
54
+ const nameSlug = (slugify as unknown as (s: string, o: object) => string)(
55
+ name,
56
+ { strict: true },
57
+ );
54
58
  const targetFolder = options.destDir || version;
55
- const remoteDir = `/www/res/sites/news-projects/${nameSlug}/${targetFolder}/`;
56
- const publicUrl = `https://www.abc.net.au/res/sites/news-projects/${nameSlug}/${targetFolder}/`;
57
- log.info(`${pc.bold("Remote dir:")} ${pc.dim(remoteDir)}`);
59
+ const remoteDir = path.join(FTP_PROJECTS_PATH, nameSlug, targetFolder, "/");
60
+ const publicUrl = `${PUBLIC_PROJECTS_URL}${nameSlug}/${targetFolder}/`;
58
61
 
59
62
  // 4. File Inventory & Size Check
60
63
  let inventory;
61
64
  try {
62
65
  inventory = await getFileInventory(localDir);
63
- } catch (err) {
66
+ } catch {
64
67
  cancel(
65
68
  `Build directory not found at ${pc.cyan(localDir)}. Have you run the build command?`,
66
69
  );
@@ -73,7 +76,10 @@ export async function run(options: DeployOptions = {}): Promise<number> {
73
76
  }
74
77
 
75
78
  const list = inventory
76
- .map((f) => ` ${pc.dim(f.relPath)} (${formatSize(f.size)})`)
79
+ .map(
80
+ (f: { relPath: string; size: number }) =>
81
+ ` ${pc.dim(f.relPath)} (${formatSize(f.size)})`,
82
+ )
77
83
  .join("\n");
78
84
  log.step(`Found ${pc.bold(inventory.length)} files to deploy:\n${list}`);
79
85
 
@@ -85,9 +91,11 @@ export async function run(options: DeployOptions = {}): Promise<number> {
85
91
  // 5. Credential Test & Confirmation
86
92
  const ftpClient = new FtpClient();
87
93
  try {
88
- await ftpClient.testConnection();
94
+ await ftpClient.connect();
89
95
  } catch (err) {
90
- // FtpClient.testConnection() already handles UI feedback via its own spinner
96
+ log.error(
97
+ `FTP connection failed: ${err instanceof Error ? err.message : String(err)}`,
98
+ );
91
99
  return 1;
92
100
  }
93
101
 
@@ -102,7 +110,7 @@ export async function run(options: DeployOptions = {}): Promise<number> {
102
110
  initialValue: false,
103
111
  });
104
112
 
105
- if (!shouldOverwrite || typeof shouldOverwrite === "symbol") {
113
+ if (!shouldOverwrite || isCancel(shouldOverwrite)) {
106
114
  cancel("Deploy cancelled.");
107
115
  return 0;
108
116
  }
@@ -132,7 +140,8 @@ export async function run(options: DeployOptions = {}): Promise<number> {
132
140
  uploadSpinner.message(`${countStr}/${totalFilesStr} ${info.name}`);
133
141
  }
134
142
  });
135
- uploadSpinner.stop("Upload complete");
143
+
144
+ uploadSpinner.stop(`Upload finished`);
136
145
  } catch (err) {
137
146
  uploadSpinner.cancel("Upload failed");
138
147
  ftpClient.close();
@@ -141,7 +150,6 @@ export async function run(options: DeployOptions = {}): Promise<number> {
141
150
 
142
151
  ftpClient.close();
143
152
 
144
- log.info(`${pc.bold("Public URL:")} ${pc.cyan(publicUrl)}`);
145
- outro(pc.green("Deploy complete!"));
153
+ outro(`${pc.bold("Released to:")} ${pc.cyan(publicUrl)}`);
146
154
  return 0;
147
155
  }
@@ -0,0 +1,320 @@
1
+ /**
2
+ * @file
3
+ * A command to migrate an old Aunty Webpack project to Aunty Vite.
4
+ *
5
+ */
6
+
7
+ import { intro, outro, confirm, log, cancel, isCancel } from "@clack/prompts";
8
+ import path from "node:path";
9
+ import fs from "node:fs/promises";
10
+ import pc from "picocolors";
11
+ import { $, glob } from "zx";
12
+ import { getHeader, spin } from "../../lib/terminal.ts";
13
+ import { findProjectDetails, loadJson } from "../../lib/util.ts";
14
+ import * as git from "../../lib/git.ts";
15
+ import { installAunty } from "../../lib/initHelpers.ts";
16
+
17
+ interface MigrateOptions {
18
+ yes?: boolean;
19
+ skipGit?: boolean;
20
+ }
21
+
22
+ /**
23
+ * Migrates a legacy HTML file by removing legacy scripts and comments,
24
+ * injecting the new Vite entry script, writing it to the destination, and deleting the source.
25
+ */
26
+ async function migrateHtmlFile(
27
+ srcPath: string,
28
+ destPath: string,
29
+ entryFile: string,
30
+ ): Promise<void> {
31
+ let content = await fs.readFile(srcPath, "utf-8");
32
+
33
+ // Remove legacy script tags (e.g. referencing index.js, index_modern.js, etc.)
34
+ content = content.replace(
35
+ /<script\s+src=["'](?:index\.js|index_modern\.js)["']\s*><\/script>/gi,
36
+ "",
37
+ );
38
+ // Remove all HTML comments
39
+ content = content.replace(/<!--[\s\S]*?-->/g, "");
40
+
41
+ // Update inline script tags that dispatch 'odyssey:api' to be modules so they run after deferred module entry points.
42
+ content = content.replace(/<script([\s\S]*?)>([\s\S]*?)<\/script>/gi, (match, attributes, innerContent) => {
43
+ const hasOdysseyApi = innerContent.includes("odyssey:api");
44
+ const isClassicScript = !attributes.trim() || attributes.includes('type="text/javascript"') || attributes.includes("type='text/javascript'");
45
+
46
+ if (hasOdysseyApi && isClassicScript) {
47
+ return `<script type="module">${innerContent}</script>`;
48
+ }
49
+ return match;
50
+ });
51
+
52
+ // Inject the new Vite entry point
53
+ const scriptTag = `<script type="module" src="/${entryFile}"></script>`;
54
+ if (content.includes("</head>")) {
55
+ content = content.replace("</head>", ` ${scriptTag}\n </head>`);
56
+ } else {
57
+ content = scriptTag + "\n" + content;
58
+ }
59
+
60
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
61
+ await fs.writeFile(destPath, content);
62
+ await fs.unlink(srcPath);
63
+ }
64
+
65
+ /**
66
+ * The main entry point for the 'aunty migrate' command.
67
+ */
68
+ export async function run(options: MigrateOptions = {}): Promise<number> {
69
+ intro(getHeader(pc.dim("aunty"), "migrate", { colour: "rainbow" }));
70
+
71
+ // 1. Load Project Details & Sanity Check on package.json presence
72
+ const details = await findProjectDetails(process.cwd());
73
+ if (!details) {
74
+ cancel(
75
+ "Could not find package.json. Please run this command inside an aunty project.",
76
+ );
77
+ return 1;
78
+ }
79
+ const baseDir = details.root;
80
+
81
+ // Check for aunty.config.js in the root of the repo
82
+ const configPath = path.join(baseDir, "aunty.config.js");
83
+ const hasConfig = await fs.access(configPath).then(() => true).catch(() => false);
84
+ if (hasConfig) {
85
+ log.error(
86
+ `Found legacy ${pc.cyan("aunty.config.js")} at the root of the project. We cannot migrate this project automatically. Please delete or rename the file before continuing, and configure Vite manually after migration.`
87
+ );
88
+ return 1;
89
+ }
90
+
91
+ // 2. Git Safety Checks (bypassed if options.skipGit is true)
92
+ if (!options.skipGit) {
93
+ const gitAccessible = await git.isAccessible();
94
+ const insideRepo = gitAccessible ? await git.isInsideRepo() : false;
95
+
96
+ if (!gitAccessible) {
97
+ log.error(
98
+ "Git is not accessible (please ensure git is installed and Xcode/licenses are accepted).",
99
+ );
100
+ return 1;
101
+ }
102
+
103
+ if (!insideRepo) {
104
+ log.error("The current directory is not a git repository.");
105
+ return 1;
106
+ }
107
+
108
+ // Check for uncommitted changes
109
+ if (!(await git.isClean())) {
110
+ log.error(
111
+ "You have uncommitted changes. Please commit or stash them before migrating.",
112
+ );
113
+ return 1;
114
+ }
115
+
116
+ // Check remote sync
117
+ if (await git.hasRemote()) {
118
+ if (await git.isBehindRemote()) {
119
+ log.error(
120
+ "Your local branch is behind the remote. Please pull the latest changes before migrating.",
121
+ );
122
+ return 1;
123
+ }
124
+ }
125
+ }
126
+
127
+ // 3. Sanity Checks for index.html and index entrypoint
128
+ const publicHtmlPath = path.join(baseDir, "public/index.html");
129
+ try {
130
+ await fs.access(publicHtmlPath);
131
+ } catch {
132
+ log.error(
133
+ `Could not find public/index.html. This project does not seem to match the expected legacy structure.`,
134
+ );
135
+ return 1;
136
+ }
137
+
138
+ const hasTS = await fs
139
+ .access(path.join(baseDir, "src/index.ts"))
140
+ .then(() => true)
141
+ .catch(() => false);
142
+ const hasJS = await fs
143
+ .access(path.join(baseDir, "src/index.js"))
144
+ .then(() => true)
145
+ .catch(() => false);
146
+
147
+ if (!hasTS && !hasJS) {
148
+ log.error("Could not find src/index.ts or src/index.js entry point.");
149
+ return 1;
150
+ }
151
+
152
+ const entryFile = hasTS ? "src/index.ts" : "src/index.js";
153
+
154
+ log.message("Attempt to migrate an Aunty Webpack project to Aunty Vite.");
155
+ log.message(
156
+ "This is a fairly naive and destructive process, and may not work for your specific configuration.",
157
+ );
158
+ log.message(
159
+ "Please make take a backup or ensure Git is pushed before proceeding.",
160
+ );
161
+
162
+ // 4. Initial Confirmation Prompt (bypassed if options.yes is true)
163
+ if (!options.yes) {
164
+ const shouldContinue = await confirm({
165
+ message: "Are you sure you want to migrate this project to Vite?",
166
+ initialValue: true,
167
+ });
168
+
169
+ if (!shouldContinue || isCancel(shouldContinue)) {
170
+ cancel("Migration cancelled.");
171
+ return 0;
172
+ }
173
+ }
174
+
175
+ // 5. Migration Execution
176
+ const migrationSpinner = spin("Migrating configuration files...");
177
+ try {
178
+ const templateDir = path.resolve(
179
+ import.meta.dirname,
180
+ "../../../templates/svelte/base/contents",
181
+ );
182
+
183
+ // Copy Vite config files
184
+ const filesToCopy = [
185
+ "vite.config.ts",
186
+ "svelte.config.js",
187
+ "tsconfig.json",
188
+ "tsconfig.app.json",
189
+ "tsconfig.node.json",
190
+ ];
191
+
192
+ for (const file of filesToCopy) {
193
+ const srcPath = path.join(templateDir, file);
194
+ const destPath = path.join(baseDir, file);
195
+ await fs.copyFile(srcPath, destPath);
196
+ }
197
+
198
+ // Merge .gitignore
199
+ const templateGitignorePath = path.join(templateDir, "_gitignore");
200
+ const targetGitignorePath = path.join(baseDir, ".gitignore");
201
+ let existingGitignore = "";
202
+ try {
203
+ existingGitignore = await fs.readFile(targetGitignorePath, "utf-8");
204
+ } catch {}
205
+ const templateGitignore = await fs.readFile(templateGitignorePath, "utf-8");
206
+
207
+ const uniqueRules = Array.from(
208
+ new Set(
209
+ [
210
+ ...existingGitignore.split(/\r?\n/),
211
+ ...templateGitignore.split(/\r?\n/),
212
+ ]
213
+ .map((line) => line.trim())
214
+ .filter((line) => line && !line.startsWith("#")),
215
+ ),
216
+ );
217
+ await fs.writeFile(targetGitignorePath, uniqueRules.join("\n") + "\n");
218
+
219
+ // Move index.html
220
+ const destHtmlPath = path.join(baseDir, "index.html");
221
+ await migrateHtmlFile(publicHtmlPath, destHtmlPath, entryFile);
222
+
223
+ // Check for legacy builder in public/builder/index.html
224
+ const publicBuilderHtmlPath = path.join(baseDir, "public/builder/index.html");
225
+ const hasLegacyBuilder = await fs
226
+ .access(publicBuilderHtmlPath)
227
+ .then(() => true)
228
+ .catch(() => false);
229
+
230
+ if (hasLegacyBuilder) {
231
+ const destBuilderHtmlPath = path.join(baseDir, "builder/index.html");
232
+ await migrateHtmlFile(publicBuilderHtmlPath, destBuilderHtmlPath, entryFile);
233
+
234
+ // Clean up legacy builder directory inside public if it exists and is empty
235
+ try {
236
+ await fs.rmdir(path.join(baseDir, "public/builder"));
237
+ } catch {}
238
+ }
239
+
240
+ migrationSpinner.stop(
241
+ "Configuration files updated and index.html migrated successfully" +
242
+ (hasLegacyBuilder ? " (including builder)" : ""),
243
+ );
244
+ } catch (err: any) {
245
+ migrationSpinner.cancel("Failed during file migration step");
246
+ log.error(err.message || String(err));
247
+ return 1;
248
+ }
249
+
250
+ // 6. Update package.json
251
+ const pkgSpinner = spin("Updating package.json dependencies and scripts...");
252
+ try {
253
+ const templateDir = path.resolve(
254
+ import.meta.dirname,
255
+ "../../../templates/svelte/base/contents",
256
+ );
257
+ const pkgPath = path.join(baseDir, "package.json");
258
+ const pkg = await loadJson<any>(pkgPath);
259
+ const templatePkg = await loadJson<any>(
260
+ path.join(templateDir, "package.json"),
261
+ );
262
+
263
+ pkg.type = "module";
264
+
265
+ // Update scripts
266
+ if (!pkg.scripts) pkg.scripts = {};
267
+ pkg.scripts.dev = "vite serve";
268
+ pkg.scripts.build = "vite build";
269
+ pkg.scripts.check =
270
+ "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json";
271
+ pkg.scripts.format = "prettier --write .";
272
+ if (pkg.scripts.start === "aunty serve") {
273
+ delete pkg.scripts.start;
274
+ }
275
+
276
+ // Merge dependencies & devDependencies
277
+ if (!pkg.dependencies) pkg.dependencies = {};
278
+ if (!pkg.devDependencies) pkg.devDependencies = {};
279
+
280
+ const oldDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
281
+ const hasSassDep = "sass" in oldDeps || "node-sass" in oldDeps;
282
+ const scssFiles = await glob("src/**/*.s[ac]ss", { cwd: baseDir });
283
+ const hasSass = hasSassDep || scssFiles.length > 0;
284
+
285
+ Object.assign(pkg.dependencies, templatePkg.dependencies || {});
286
+ Object.assign(pkg.devDependencies, templatePkg.devDependencies || {});
287
+
288
+ if (hasSass) {
289
+ pkg.devDependencies["sass-embedded"] = "^1.83.0";
290
+ }
291
+
292
+ // Save package.json
293
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
294
+
295
+ // Set @abcnews/aunty to latest or local file
296
+ await installAunty(baseDir);
297
+
298
+ pkgSpinner.stop("package.json updated");
299
+ } catch (err: any) {
300
+ pkgSpinner.cancel("Failed to update package.json");
301
+ log.error(err.message || String(err));
302
+ return 1;
303
+ }
304
+
305
+ // 7. NPM Install
306
+ const installSpinner = spin("Installing dependencies via npm...");
307
+ try {
308
+ await $({ cwd: baseDir })`npm install`.quiet();
309
+ installSpinner.stop("Dependencies installed");
310
+ } catch (err: any) {
311
+ installSpinner.cancel("Failed to install dependencies.");
312
+ log.error(`npm install failed: ${err.message || String(err)}`);
313
+ return 1;
314
+ }
315
+
316
+ outro(
317
+ "Migration completed successfully! You can now run 'npm run dev' to start development.",
318
+ );
319
+ return 0;
320
+ }
@@ -0,0 +1,127 @@
1
+ import { intro, outro, select, cancel, isCancel, log } from "@clack/prompts";
2
+ import pc from "picocolors";
3
+ import semver from "semver";
4
+ import { $ } from "zx";
5
+ import { getHeader, spin } from "../../lib/terminal.ts";
6
+ import { findProjectDetails } from "../../lib/util.ts";
7
+ import { getVersionOptions } from "../../lib/semver.ts";
8
+ import { run as runReleaseCheck } from "../release-check/index.ts";
9
+ import { run as runDeploy } from "../deploy/index.ts";
10
+ import * as git from "../../lib/git.ts";
11
+
12
+ interface ReleaseOptions {
13
+ version?: string;
14
+ }
15
+
16
+ /**
17
+ * The main entry point for the 'aunty release' command.
18
+ */
19
+ export async function run(options: ReleaseOptions = {}): Promise<number> {
20
+ intro(getHeader(pc.dim("aunty"), "release", { colour: "yellow" }));
21
+
22
+ // 1. Load project details
23
+ const details = await findProjectDetails(process.cwd());
24
+ if (!details) {
25
+ return 1;
26
+ }
27
+
28
+ const { pkg } = details;
29
+ const currentVersion = pkg.version;
30
+
31
+ if (!currentVersion) {
32
+ cancel("No version found in package.json, please set up your project.");
33
+ return 1;
34
+ }
35
+
36
+ if (!semver.valid(currentVersion)) {
37
+ cancel(
38
+ `Current version ${pc.bold(currentVersion)} is not a valid semver version`,
39
+ );
40
+ return 1;
41
+ }
42
+
43
+ let nextVersion: string | undefined = undefined;
44
+
45
+ // 2. Handle override version option if provided
46
+ if (options.version) {
47
+ if (!semver.valid(options.version)) {
48
+ cancel(
49
+ `The version specified (${pc.bold(options.version)}) is not a valid semver version`,
50
+ );
51
+ return 1;
52
+ }
53
+ nextVersion = options.version;
54
+ log.info(`Using specified version: ${pc.green(nextVersion)}`);
55
+ }
56
+
57
+ // 3. Determine version bump options dynamically
58
+ if (!nextVersion) {
59
+ const selectOptions = getVersionOptions(currentVersion);
60
+
61
+ const bumpSelection = await select({
62
+ message: "What package version bump is this release?",
63
+ options: selectOptions,
64
+ });
65
+
66
+ if (isCancel(bumpSelection)) {
67
+ cancel("Release cancelled");
68
+ return 1;
69
+ }
70
+
71
+ nextVersion = bumpSelection as string;
72
+ }
73
+
74
+ // 4. Run pre-release checks
75
+ const checkResult = await runReleaseCheck({
76
+ skipHeader: true,
77
+ quiet: true,
78
+ projectName: details.pkg.name,
79
+ version: nextVersion,
80
+ });
81
+
82
+ if (checkResult !== 0) {
83
+ outro(pc.red("Release checks failed."));
84
+ return checkResult;
85
+ }
86
+
87
+ // 5. Bump version locally
88
+ try {
89
+ await $`npm version ${nextVersion}`;
90
+ } catch (err) {
91
+ outro(
92
+ pc.red(
93
+ `Failed to set version: ${err instanceof Error ? err.message : String(err)}`,
94
+ ),
95
+ );
96
+ return 1;
97
+ }
98
+
99
+ // 6. Push local changes and version tag to remote
100
+ const pushSpinner = spin("Pushing release & tag to remote...");
101
+ try {
102
+ await $`git push origin HEAD --follow-tags`.quiet();
103
+ pushSpinner.stop("Pushed to remote");
104
+ } catch (err) {
105
+ pushSpinner.cancel("Push failed");
106
+ outro(
107
+ pc.red(
108
+ `Failed to push to remote: ${err instanceof Error ? err.message : String(err)}`,
109
+ ),
110
+ );
111
+ await git.rollback(nextVersion);
112
+ return 1;
113
+ }
114
+
115
+ // 7. Deploy the release
116
+ const deployResult = await runDeploy({ skipHeader: true });
117
+
118
+ if (deployResult !== 0) {
119
+ outro(
120
+ pc.red(
121
+ "Deployment failed. You can retry manually with `npm run deploy`.",
122
+ ),
123
+ );
124
+ return deployResult;
125
+ }
126
+ return 0;
127
+ }
@@ -8,7 +8,19 @@ export async function isAccessible(): Promise<boolean> {
8
8
  try {
9
9
  await $`git --version`.quiet();
10
10
  return true;
11
- } catch (err) {
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Checks if the current directory is an initialized git repository.
18
+ */
19
+ export async function isInsideRepo(): Promise<boolean> {
20
+ try {
21
+ await $`git rev-parse --is-inside-work-tree`.quiet();
22
+ return true;
23
+ } catch {
12
24
  return false;
13
25
  }
14
26
  }
@@ -45,7 +57,7 @@ export async function hasRemote(): Promise<boolean> {
45
57
  try {
46
58
  await $`git rev-parse --abbrev-ref @{u}`.quiet();
47
59
  return true;
48
- } catch (err) {
60
+ } catch {
49
61
  return false;
50
62
  }
51
63
  }