hatchkit 0.1.4 → 0.1.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 (63) hide show
  1. package/dist/adopt.d.ts +2 -0
  2. package/dist/adopt.d.ts.map +1 -0
  3. package/dist/adopt.js +552 -0
  4. package/dist/adopt.js.map +1 -0
  5. package/dist/completion.d.ts.map +1 -1
  6. package/dist/completion.js +3 -0
  7. package/dist/completion.js.map +1 -1
  8. package/dist/config.d.ts +30 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +108 -0
  11. package/dist/config.js.map +1 -1
  12. package/dist/deploy/pages.js +50 -9
  13. package/dist/deploy/pages.js.map +1 -1
  14. package/dist/deploy/rename-domain.d.ts.map +1 -1
  15. package/dist/deploy/rename-domain.js +26 -6
  16. package/dist/deploy/rename-domain.js.map +1 -1
  17. package/dist/deploy/rollback.d.ts +10 -0
  18. package/dist/deploy/rollback.d.ts.map +1 -0
  19. package/dist/deploy/rollback.js +295 -0
  20. package/dist/deploy/rollback.js.map +1 -0
  21. package/dist/deploy/terraform.d.ts +10 -1
  22. package/dist/deploy/terraform.d.ts.map +1 -1
  23. package/dist/deploy/terraform.js +177 -42
  24. package/dist/deploy/terraform.js.map +1 -1
  25. package/dist/doctor.d.ts.map +1 -1
  26. package/dist/doctor.js +25 -0
  27. package/dist/doctor.js.map +1 -1
  28. package/dist/explain.d.ts.map +1 -1
  29. package/dist/explain.js +5 -0
  30. package/dist/explain.js.map +1 -1
  31. package/dist/index.js +356 -122
  32. package/dist/index.js.map +1 -1
  33. package/dist/prompts.d.ts.map +1 -1
  34. package/dist/prompts.js +283 -11
  35. package/dist/prompts.js.map +1 -1
  36. package/dist/provision/stripe.d.ts +19 -0
  37. package/dist/provision/stripe.d.ts.map +1 -0
  38. package/dist/provision/stripe.js +58 -0
  39. package/dist/provision/stripe.js.map +1 -0
  40. package/dist/scaffold/dotenvx.d.ts.map +1 -1
  41. package/dist/scaffold/dotenvx.js +35 -11
  42. package/dist/scaffold/dotenvx.js.map +1 -1
  43. package/dist/scaffold/infra.d.ts +21 -1
  44. package/dist/scaffold/infra.d.ts.map +1 -1
  45. package/dist/scaffold/infra.js +66 -20
  46. package/dist/scaffold/infra.js.map +1 -1
  47. package/dist/status.d.ts.map +1 -1
  48. package/dist/status.js +7 -0
  49. package/dist/status.js.map +1 -1
  50. package/dist/utils/coolify-api.d.ts +12 -0
  51. package/dist/utils/coolify-api.d.ts.map +1 -1
  52. package/dist/utils/coolify-api.js +33 -0
  53. package/dist/utils/coolify-api.js.map +1 -1
  54. package/dist/utils/run-ledger.d.ts +68 -0
  55. package/dist/utils/run-ledger.d.ts.map +1 -0
  56. package/dist/utils/run-ledger.js +99 -0
  57. package/dist/utils/run-ledger.js.map +1 -0
  58. package/dist/utils/secrets.d.ts +2 -0
  59. package/dist/utils/secrets.d.ts.map +1 -1
  60. package/dist/utils/secrets.js +2 -0
  61. package/dist/utils/secrets.js.map +1 -1
  62. package/package.json +2 -2
  63. package/scripts/release-prep.mjs +56 -98
@@ -0,0 +1,99 @@
1
+ /*
2
+ * Run ledger — append-only record of what `hatchkit create` accomplished
3
+ * for one project. Persisted as JSON next to the Conf store so it
4
+ * survives crashes and lets us:
5
+ *
6
+ * 1. On failure mid-create, print a tailored cleanup recipe and offer
7
+ * to undo the steps that did succeed.
8
+ * 2. Drive `hatchkit destroy <project>` later — the same code path,
9
+ * just without the "should I?" prompt.
10
+ *
11
+ * Steps are recorded *immediately after each external mutation* so a
12
+ * SIGKILL between operations leaves an accurate (though possibly
13
+ * incomplete) ledger. Reverse-order undo is the cleanup strategy.
14
+ *
15
+ * Path: <configDir>/runs/<sanitized-name>.json
16
+ */
17
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+ import { getStore } from "../config.js";
20
+ /** Sanitize project name for use as a filename. Project names are
21
+ * already validated to be slug-shaped, but belt-and-braces. */
22
+ function sanitize(name) {
23
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
24
+ }
25
+ function runsDir() {
26
+ return join(dirname(getStore().path), "runs");
27
+ }
28
+ function ledgerPath(name) {
29
+ return join(runsDir(), `${sanitize(name)}.json`);
30
+ }
31
+ export class RunLedger {
32
+ _path;
33
+ data;
34
+ constructor(_path, data) {
35
+ this._path = _path;
36
+ this.data = data;
37
+ }
38
+ /** Begin a new ledger for this project. Overwrites any prior ledger
39
+ * for the same name (assumes the caller has already cleaned up). */
40
+ static start(name) {
41
+ const data = {
42
+ name,
43
+ startedAt: new Date().toISOString(),
44
+ steps: [],
45
+ };
46
+ const path = ledgerPath(name);
47
+ mkdirSync(dirname(path), { recursive: true });
48
+ writeFileSync(path, JSON.stringify(data, null, 2));
49
+ return new RunLedger(path, data);
50
+ }
51
+ /** Load the ledger for a project, if one exists. */
52
+ static load(name) {
53
+ const path = ledgerPath(name);
54
+ if (!existsSync(path))
55
+ return null;
56
+ try {
57
+ const data = JSON.parse(readFileSync(path, "utf-8"));
58
+ return new RunLedger(path, data);
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ /** Append a step and flush immediately. */
65
+ record(step) {
66
+ this.data.steps.push(step);
67
+ this.flush();
68
+ }
69
+ /** Mark the run finished. The ledger sticks around so `hatchkit
70
+ * destroy` can use it later. */
71
+ complete() {
72
+ this.data.finishedAt = new Date().toISOString();
73
+ this.flush();
74
+ }
75
+ /** Remove the ledger from disk. Call after a successful rollback. */
76
+ delete() {
77
+ if (existsSync(this._path))
78
+ unlinkSync(this._path);
79
+ }
80
+ get name() {
81
+ return this.data.name;
82
+ }
83
+ get steps() {
84
+ return this.data.steps;
85
+ }
86
+ get startedAt() {
87
+ return this.data.startedAt;
88
+ }
89
+ get finishedAt() {
90
+ return this.data.finishedAt;
91
+ }
92
+ get path() {
93
+ return this._path;
94
+ }
95
+ flush() {
96
+ writeFileSync(this._path, JSON.stringify(this.data, null, 2));
97
+ }
98
+ }
99
+ //# sourceMappingURL=run-ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-ledger.js","sourceRoot":"","sources":["../../src/utils/run-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AA0BxC;gEACgE;AAChE,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,OAAO;IACd,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;AAChD,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,OAAO,SAAS;IAED;IACT;IAFV,YACmB,KAAa,EACtB,IAAgB;QADP,UAAK,GAAL,KAAK,CAAQ;QACtB,SAAI,GAAJ,IAAI,CAAY;IACvB,CAAC;IAEJ;yEACqE;IACrE,MAAM,CAAC,KAAK,CAAC,IAAY;QACvB,MAAM,IAAI,GAAe;YACvB,IAAI;YACJ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,KAAK,EAAE,EAAE;SACV,CAAC;QACF,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAC9B,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QACnD,OAAO,IAAI,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,oDAAoD;IACpD,MAAM,CAAC,IAAI,CAAC,IAAY;QACtB,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACnC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAe,CAAC;YACnE,OAAO,IAAI,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,2CAA2C;IAC3C,MAAM,CAAC,IAAgB;QACrB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,KAAK,EAAE,CAAC;IACf,CAAC;IAED;qCACiC;IACjC,QAAQ;QACN,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QAChD,IAAI,CAAC,KAAK,EAAE,CAAC;IACf,CAAC;IAED,qEAAqE;IACrE,MAAM;QACJ,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrD,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IACxB,CAAC;IAED,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;IACzB,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC;IAC7B,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC;IAC9B,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAEO,KAAK;QACX,aAAa,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAChE,CAAC;CACF"}
@@ -19,6 +19,8 @@ export declare const SECRET_KEYS: {
19
19
  readonly openpanelRootClientSecret: "openpanel:root-client-secret";
20
20
  readonly openpanelClientSecret: (name: string) => string;
21
21
  readonly resendApiKey: "resend:api-key";
22
+ readonly stripeSecretKey: "stripe:secret-key";
23
+ readonly stripePublishableKey: "stripe:publishable-key";
22
24
  /** Per-scaffolded-project dotenvx private key for .env.production.
23
25
  * Stored in the OS keychain so the CLI's on-disk state never holds
24
26
  * decryption material for the starter's encrypted env. */
@@ -1 +1 @@
1
- {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/utils/secrets.ts"],"names":[],"mappings":"AAoBA;mEACmE;AACnE,eAAO,MAAM,WAAW;;;;;IAKtB;;oDAEgD;;qCAExB,MAAM;qCACN,MAAM;mCACR,MAAM;;IAE5B;wEACoE;;;2CAGtC,MAAM;;IAEpC;;+DAE2D;8CAC1B,MAAM;CAC/B,CAAC;AAEX,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnE;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEhE;AAED,iEAAiE;AACjE,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAGrD"}
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/utils/secrets.ts"],"names":[],"mappings":"AAoBA;mEACmE;AACnE,eAAO,MAAM,WAAW;;;;;IAKtB;;oDAEgD;;qCAExB,MAAM;qCACN,MAAM;mCACR,MAAM;;IAE5B;wEACoE;;;2CAGtC,MAAM;;;;IAIpC;;+DAE2D;8CAC1B,MAAM;CAC/B,CAAC;AAEX,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnE;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEhE;AAED,iEAAiE;AACjE,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAGrD"}
@@ -36,6 +36,8 @@ export const SECRET_KEYS = {
36
36
  openpanelRootClientSecret: "openpanel:root-client-secret",
37
37
  openpanelClientSecret: (name) => `openpanel:${name}:client-secret`,
38
38
  resendApiKey: "resend:api-key",
39
+ stripeSecretKey: "stripe:secret-key",
40
+ stripePublishableKey: "stripe:publishable-key",
39
41
  /** Per-scaffolded-project dotenvx private key for .env.production.
40
42
  * Stored in the OS keychain so the CLI's on-disk state never holds
41
43
  * decryption material for the starter's encrypted env. */
@@ -1 +1 @@
1
- {"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/utils/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,iEAAiE;AACjE,uEAAuE;AACvE,sEAAsE;AACtE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,UAAU,CAAC;AAElE;mEACmE;AACnE,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,eAAe,EAAE,mBAAmB;IACpC,kBAAkB,EAAE,sBAAsB;IAC1C;;oDAEgD;IAChD,wBAAwB,EAAE,6BAA6B;IACvD,WAAW,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,MAAM,QAAQ,aAAa;IAC9D,WAAW,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,MAAM,QAAQ,aAAa;IAC9D,SAAS,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,OAAO,QAAQ,UAAU;IAC1D,cAAc,EAAE,sBAAsB;IACtC;wEACoE;IACpE,qBAAqB,EAAE,0BAA0B;IACjD,yBAAyB,EAAE,8BAA8B;IACzD,qBAAqB,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,aAAa,IAAI,gBAAgB;IAC1E,YAAY,EAAE,gBAAgB;IAC9B;;+DAE2D;IAC3D,iBAAiB,EAAE,CAAC,WAAmB,EAAE,EAAE,CAAC,WAAW,WAAW,yBAAyB;CACnF,CAAC;AAEX,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW;IACzC,OAAO,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,KAAa;IACxD,MAAM,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAW;IAC5C,OAAO,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAC7C,CAAC;AAED,iEAAiE;AACjE,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IACtD,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AACnF,CAAC"}
1
+ {"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/utils/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,iEAAiE;AACjE,uEAAuE;AACvE,sEAAsE;AACtE,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,UAAU,CAAC;AAElE;mEACmE;AACnE,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,eAAe,EAAE,mBAAmB;IACpC,kBAAkB,EAAE,sBAAsB;IAC1C;;oDAEgD;IAChD,wBAAwB,EAAE,6BAA6B;IACvD,WAAW,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,MAAM,QAAQ,aAAa;IAC9D,WAAW,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,MAAM,QAAQ,aAAa;IAC9D,SAAS,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,OAAO,QAAQ,UAAU;IAC1D,cAAc,EAAE,sBAAsB;IACtC;wEACoE;IACpE,qBAAqB,EAAE,0BAA0B;IACjD,yBAAyB,EAAE,8BAA8B;IACzD,qBAAqB,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,aAAa,IAAI,gBAAgB;IAC1E,YAAY,EAAE,gBAAgB;IAC9B,eAAe,EAAE,mBAAmB;IACpC,oBAAoB,EAAE,wBAAwB;IAC9C;;+DAE2D;IAC3D,iBAAiB,EAAE,CAAC,WAAmB,EAAE,EAAE,CAAC,WAAW,WAAW,yBAAyB;CACnF,CAAC;AAEX,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW;IACzC,OAAO,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAW,EAAE,KAAa;IACxD,MAAM,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAW;IAC5C,OAAO,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAC7C,CAAC;AAED,iEAAiE;AACjE,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IACtD,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AACnF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hatchkit",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "packageManager": "pnpm@10.33.2",
5
5
  "description": "Interactive CLI for scaffolding full-stack projects and provisioning observability/email clients",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "release:patch": "node scripts/release-prep.mjs && npm version patch -m \"chore: release v%s\" && pnpm run _release:finish",
28
28
  "release:minor": "node scripts/release-prep.mjs && npm version minor -m \"chore: release v%s\" && pnpm run _release:finish",
29
29
  "release:major": "node scripts/release-prep.mjs && npm version major -m \"chore: release v%s\" && pnpm run _release:finish",
30
- "_release:finish": "pnpm run build && pnpm run typecheck && npm publish --access public && git push --follow-tags && npm install -g .",
30
+ "_release:finish": "pnpm run build && pnpm run typecheck && npm publish --access public && git -C ../infra push && git -C ../starter push && git push --follow-tags && npm install -g .",
31
31
  "prepublishOnly": "pnpm run build"
32
32
  },
33
33
  "dependencies": {
@@ -1,30 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * release-prep — run before `npm version` so any dangling working-tree
4
- * changes get committed and shipped with the release instead of hanging
5
- * around on main afterwards.
3
+ * release-prep — strict pre-release verification.
6
4
  *
7
- * Runs from cli/ (where the release scripts live). Finds the repo root
8
- * via `git rev-parse --show-toplevel` and operates on the whole repo,
9
- * not just cli/ if you've left changes under infra/ or docs/ they get
10
- * picked up too.
5
+ * Refuses to release if any of the three repos (hatchkit main,
6
+ * infra submodule, starter submodule) has uncommitted or untracked
7
+ * changes. Exits with a clear list of which repos need handling and
8
+ * what's dirty in each — release scripts assume a clean baseline so
9
+ * the version commit + tag, the npm publish, and the cross-repo push
10
+ * all line up against the same set of trees.
11
11
  *
12
- * Safety rails:
13
- * - Refuses to run if any staged/untracked path looks like a secret
14
- * (.env*, *.pem, *.key, *credentials*, *secret*).
15
- * - Submodule pointer changes (git shows as ` M infra` vs `M infra`)
16
- * are fine, but untracked content INSIDE a submodule is NOT auto-
17
- * committed from the parent repo — you'd want to commit that inside
18
- * the submodule yourself first. We warn + bail in that case.
19
- * - Interactive: prompts for a commit message (default:
20
- * "chore: pre-release changes").
21
- * - Non-interactive (no TTY): uses the default message and proceeds.
22
- * Skip the whole thing with RELEASE_SKIP_PREP=1.
12
+ * Skip with RELEASE_SKIP_PREP=1 (e.g. a CI-driven release that's
13
+ * already vouched for cleanliness).
23
14
  */
24
15
 
25
- import { execSync, spawnSync } from "node:child_process";
26
- import { createInterface } from "node:readline";
27
- import { stdin as input, stdout as output } from "node:process";
16
+ import { execSync } from "node:child_process";
17
+ import { existsSync } from "node:fs";
18
+ import { join, relative } from "node:path";
28
19
 
29
20
  if (process.env.RELEASE_SKIP_PREP === "1") {
30
21
  console.log(" release-prep: RELEASE_SKIP_PREP=1 — skipping.");
@@ -35,96 +26,63 @@ function sh(cmd, opts = {}) {
35
26
  return execSync(cmd, { encoding: "utf8", ...opts }).trim();
36
27
  }
37
28
 
38
- // Find the repo root so git commands work no matter where the script was
39
- // launched from (pnpm invokes scripts with cwd=cli/).
40
29
  let repoRoot;
41
30
  try {
42
31
  repoRoot = sh("git rev-parse --show-toplevel");
43
- } catch (err) {
32
+ } catch {
44
33
  console.error(" release-prep: not inside a git repo. Aborting.");
45
34
  process.exit(1);
46
35
  }
47
36
 
48
- const porcelain = sh("git status --porcelain", { cwd: repoRoot });
49
- if (!porcelain) {
50
- console.log(" release-prep: working tree clean. Nothing to commit.");
51
- process.exit(0);
52
- }
53
-
54
- const entries = porcelain.split("\n").map((l) => {
55
- // Porcelain format: "XY path". X = index, Y = worktree. Submodules
56
- // show up with `m`/`M` in position Y + 1 for content changes.
57
- const code = l.slice(0, 2);
58
- const path = l.slice(3);
59
- return { code, path };
60
- });
37
+ const repos = [
38
+ { label: "hatchkit (main)", path: repoRoot, hint: `cd ${repoRoot}` },
39
+ {
40
+ label: "infra (submodule)",
41
+ path: join(repoRoot, "infra"),
42
+ hint: `cd ${join(repoRoot, "infra")}`,
43
+ },
44
+ {
45
+ label: "starter (submodule)",
46
+ path: join(repoRoot, "starter"),
47
+ hint: `cd ${join(repoRoot, "starter")}`,
48
+ },
49
+ ];
61
50
 
62
- console.log("\n release-prep: working tree isn't clean.\n");
63
- console.log(sh("git status --short", { cwd: repoRoot }));
64
-
65
- // Refuse secret-looking paths. Keep the pattern conservative better a
66
- // false positive that requires manual commit than a leaked token.
67
- const SECRET_RE = /(^|\/)(\.env(\.|$)|[^/]*credentials[^/]*|[^/]*secret[^/]*|[^/]*\.(pem|key|pfx|p12)$)/i;
68
- const secretsFound = entries.filter((e) => SECRET_RE.test(e.path));
69
- if (secretsFound.length > 0) {
70
- console.error("\n release-prep: refusing — these paths look like secrets:\n");
71
- for (const e of secretsFound) console.error(` ${e.path}`);
72
- console.error(
73
- "\n Resolve manually: gitignore, delete, or commit yourself and re-run the release.",
74
- );
75
- process.exit(1);
76
- }
51
+ const dirty = [];
52
+ for (const r of repos) {
53
+ // Submodule may not be initialized — skip silently rather than fail.
54
+ // (`.git` is a file inside an initialized submodule, a dir at the root.)
55
+ if (!existsSync(join(r.path, ".git"))) continue;
77
56
 
78
- // Untracked content inside a submodule shows as e.g. ' m infra' with
79
- // only the second column set to lowercase 'm'. Parent-repo `git add`
80
- // can't reach inside; bail so the user commits in the submodule first.
81
- const untrackedInSubmodule = entries.filter((e) => e.code === " m" || e.code === "?m");
82
- if (untrackedInSubmodule.length > 0) {
83
- console.error(
84
- "\n release-prep: submodule has untracked changes inside it:\n",
85
- );
86
- for (const e of untrackedInSubmodule) {
87
- console.error(` ${e.path} — commit inside the submodule first`);
57
+ let status;
58
+ try {
59
+ status = sh("git status --porcelain", { cwd: r.path });
60
+ } catch (err) {
61
+ console.error(` release-prep: couldn't read ${r.label}: ${err.message}`);
62
+ process.exit(1);
63
+ }
64
+ if (status) {
65
+ dirty.push({ ...r, status });
88
66
  }
89
- console.error(
90
- "\n cd into each submodule, commit, then `git add <submodule>` + re-run the release.",
91
- );
92
- process.exit(1);
93
67
  }
94
68
 
95
- // Prompt for a commit message unless we're non-interactive.
96
- const DEFAULT_MSG = "chore: pre-release changes";
97
- let message = DEFAULT_MSG;
98
- if (input.isTTY && output.isTTY) {
99
- const rl = createInterface({ input, output });
100
- message = await new Promise((resolve) => {
101
- rl.question(` Commit message [${DEFAULT_MSG}]: `, (answer) => {
102
- rl.close();
103
- resolve(answer.trim() || DEFAULT_MSG);
104
- });
105
- });
106
- } else {
107
- console.log(` release-prep: non-interactive — using default message "${DEFAULT_MSG}".`);
69
+ if (dirty.length === 0) {
70
+ console.log(" ✓ release-prep: all trees clean. Continuing release.");
71
+ process.exit(0);
108
72
  }
109
73
 
110
- // Stage + commit at the repo root. Using `git add -A` here is intentional
111
- // (the whole point of this helper is to sweep everything into the release
112
- // commit); the secret-path guard above is what keeps it safe.
113
- const add = spawnSync("git", ["add", "-A"], { cwd: repoRoot, stdio: "inherit" });
114
- if (add.status !== 0) {
115
- console.error(" release-prep: `git add -A` failed.");
116
- process.exit(add.status ?? 1);
74
+ console.error("\n ✗ release-prep: cannot release dangling changes.\n");
75
+ for (const r of dirty) {
76
+ const rel = r.path === repoRoot ? "." : relative(repoRoot, r.path);
77
+ console.error(` ── ${r.label} (${rel}/)`);
78
+ for (const line of r.status.split("\n")) {
79
+ console.error(` ${line}`);
80
+ }
81
+ console.error();
117
82
  }
118
-
119
- const commit = spawnSync("git", ["commit", "-m", message], {
120
- cwd: repoRoot,
121
- stdio: "inherit",
122
- });
123
- if (commit.status !== 0) {
124
- console.error(
125
- " release-prep: `git commit` failed (pre-commit hook? empty diff after filters?).",
126
- );
127
- process.exit(commit.status ?? 1);
83
+ console.error(" Handle each tree before releasing:");
84
+ for (const r of dirty) {
85
+ console.error(` ${r.hint} && git status # commit / stash / discard`);
128
86
  }
129
-
130
- console.log("\n release-prep: committed. Continuing release.\n");
87
+ console.error("\n Then re-run the release.\n");
88
+ process.exit(1);