saasaloy 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,6 +51,7 @@ my-app/
51
51
  packages/db/ Drizzle schema + client (saasaloy add database-d1 | database-postgres)
52
52
  packages/auth/ Better Auth (saasaloy add auth)
53
53
  packages/email/ email provider interface (saasaloy add email)
54
+ packages/queue/ background jobs + schedules (saasaloy add queue)
54
55
  .agents/skills/ agent skills, symlinked from .claude/skills/
55
56
  DESIGN.md the design contract
56
57
  saasaloy.json installed modules + alias map
@@ -64,6 +65,7 @@ my-app/
64
65
  | Database | Drizzle ORM on D1 (SQLite) or Postgres |
65
66
  | Auth | Better Auth |
66
67
  | Email | Cloudflare Email Sending, Plunk, or a console logger |
68
+ | Background work | Cloudflare Queues and Cron Triggers, or an in-process runner |
67
69
  | Infra | wrangler per workspace, or Pulumi via the `infra` module |
68
70
  | Monorepo | Turborepo + pnpm |
69
71
 
@@ -89,9 +91,9 @@ Modules come in tiers. A **capability** scaffolds a workspace and sets conventio
89
91
 
90
92
  | Tier | Modules |
91
93
  |---|---|
92
- | Capability | `api`, `database`, `validators`, `logger`, `auth`, `admin`, `email`, `sms`, `infra` |
94
+ | Capability | `api`, `database`, `validators`, `logger`, `auth`, `admin`, `email`, `sms`, `queue`, `infra` |
93
95
  | Feature | `waitlist`, `teams`, `email-react` |
94
- | Provider | `email-console`, `email-cloudflare`, `email-plunk`, `logger-console`, `sms-console` |
96
+ | Provider | `email-console`, `email-cloudflare`, `email-plunk`, `logger-console`, `sms-console`, `queue-cloudflare`, `queue-memory` |
95
97
  | Driver | `database-d1`, `database-postgres` |
96
98
 
97
99
  `saasaloy add <name> --dry-run` prints what a module would do to your project before it does it. The one-table map of every module, what it gives you, and what it depends on is on the [Modules](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/modules.md) page.
@@ -110,8 +112,9 @@ A few modules need something the free tier does not cover:
110
112
  | `email-plunk` | a [Plunk](https://www.useplunk.com) account and `PLUNK_API_KEY` |
111
113
  | `database-postgres` | a Postgres server reachable from a Worker, with its URL in `DATABASE_URL`. Install instead of `database-d1`, never alongside |
112
114
  | `sms` | a third-party SMS account for any real send. Cloudflare has no SMS product. `sms-console` is free |
115
+ | `queue-cloudflare` | a Workers paid plan, and the two queues created once with `wrangler queues create`. Install `queue-memory` instead for local work |
113
116
 
114
- The console providers (`email-console`, `sms-console`, `logger-console`) log instead of sending, so local development needs no plan, domain, or key. Details for each are in the [Reference](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/reference.md#email-providers).
117
+ The local providers (`email-console`, `sms-console`, `logger-console`, `queue-memory`) log or run inline instead of calling a service, so local development needs no plan, domain, or key. Details for each are in the [Reference](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/reference.md#email-providers).
115
118
 
116
119
  ## Deploy
117
120
 
package/dist/index.js CHANGED
@@ -877,7 +877,8 @@ function upsertWranglerBinding(source, patch) {
877
877
  if (!root) {
878
878
  return source;
879
879
  }
880
- const arrayNode = findNodeAtLocation(root, [patch.bindingType]);
880
+ const bindingPath = splitBindingType(patch.bindingType);
881
+ const arrayNode = findNodeAtLocation(root, bindingPath);
881
882
  const formattingOptions = inferFormatting(source);
882
883
  if (arrayNode?.type === "array") {
883
884
  const existing = (arrayNode.children ?? []).map(
@@ -892,7 +893,7 @@ function upsertWranglerBinding(source, patch) {
892
893
  }
893
894
  const edits2 = modify(
894
895
  source,
895
- [patch.bindingType, existing.length],
896
+ [...bindingPath, existing.length],
896
897
  patch.entry,
897
898
  {
898
899
  formattingOptions,
@@ -901,7 +902,7 @@ function upsertWranglerBinding(source, patch) {
901
902
  );
902
903
  return applyEdits(source, edits2);
903
904
  }
904
- const edits = modify(source, [patch.bindingType], [patch.entry], {
905
+ const edits = modify(source, bindingPath, [patch.entry], {
905
906
  formattingOptions
906
907
  });
907
908
  return applyEdits(source, edits);
@@ -911,7 +912,8 @@ function removeWranglerBinding(source, patch) {
911
912
  if (!root) {
912
913
  return source;
913
914
  }
914
- const arrayNode = findNodeAtLocation(root, [patch.bindingType]);
915
+ const bindingPath = splitBindingType(patch.bindingType);
916
+ const arrayNode = findNodeAtLocation(root, bindingPath);
915
917
  if (arrayNode?.type !== "array") {
916
918
  return source;
917
919
  }
@@ -925,7 +927,7 @@ function removeWranglerBinding(source, patch) {
925
927
  if (matchWranglerBinding(source, patch)) {
926
928
  return source;
927
929
  }
928
- const path = existing.length === 1 ? [patch.bindingType] : [patch.bindingType, index];
930
+ const path = existing.length === 1 ? emptiedAncestorPath(root, bindingPath) : [...bindingPath, index];
929
931
  const edits = modify(source, path, void 0, {
930
932
  formattingOptions: inferFormatting(source)
931
933
  });
@@ -938,6 +940,20 @@ function wranglerBindingRemoveRefusal(source, patch) {
938
940
  }
939
941
  return `${match.key} holds ${JSON.stringify(match.current)} now, not the entry that was applied, so it is not ours to delete`;
940
942
  }
943
+ function splitBindingType(bindingType) {
944
+ return bindingType.split(".");
945
+ }
946
+ function emptiedAncestorPath(root, bindingPath) {
947
+ let cut = bindingPath.length;
948
+ for (let depth = bindingPath.length - 1; depth >= 1; depth -= 1) {
949
+ const parent = findNodeAtLocation(root, bindingPath.slice(0, depth));
950
+ if (parent?.type !== "object" || (parent.children?.length ?? 0) !== 1) {
951
+ break;
952
+ }
953
+ cut = depth;
954
+ }
955
+ return bindingPath.slice(0, cut);
956
+ }
941
957
  function findEntryIndex(existing, patch) {
942
958
  const { entry } = patch;
943
959
  if (typeof entry === "string") {
@@ -957,7 +973,10 @@ function matchWranglerBinding(source, patch) {
957
973
  if (!root) {
958
974
  return void 0;
959
975
  }
960
- const arrayNode = findNodeAtLocation(root, [patch.bindingType]);
976
+ const arrayNode = findNodeAtLocation(
977
+ root,
978
+ splitBindingType(patch.bindingType)
979
+ );
961
980
  if (arrayNode?.type !== "array") {
962
981
  return void 0;
963
982
  }
@@ -3667,7 +3686,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
3667
3686
  // package.json
3668
3687
  var package_default = {
3669
3688
  name: "saasaloy",
3670
- version: "0.1.1",
3689
+ version: "0.1.2",
3671
3690
  description: "Composable SaaS accelerator kit.",
3672
3691
  keywords: [
3673
3692
  "saas",
@@ -3888,6 +3907,19 @@ function recordBaseFiles(manifest, files) {
3888
3907
  function isBaseTracked(lock, manifest) {
3889
3908
  return lock.base !== void 0 && Object.keys(baseEntries(manifest)).length > 0;
3890
3909
  }
3910
+ async function missingBaseTargets(root, manifest, templateDir) {
3911
+ const seed = new Set((await readBaseDeclaration(templateDir)).seedFiles);
3912
+ const missing = [];
3913
+ for (const target of Object.keys(baseEntries(manifest))) {
3914
+ if (seed.has(target)) {
3915
+ continue;
3916
+ }
3917
+ if (await readIfPresent(resolveWithinRoot(root, target)) === void 0) {
3918
+ missing.push(target);
3919
+ }
3920
+ }
3921
+ return missing.toSorted((a, b) => a < b ? -1 : 1);
3922
+ }
3891
3923
  async function renderTemplate(root, templateDir) {
3892
3924
  const dir = await mkdtemp2(join10(tmpdir2(), "saasaloy-base-render-"));
3893
3925
  const files = await copyTemplate(
@@ -4326,7 +4358,7 @@ var HELP2 = {
4326
4358
  usage: USAGE2,
4327
4359
  flags: {}
4328
4360
  };
4329
- var DEFAULT_PATH = "modules";
4361
+ var DEFAULT_PATH = ".";
4330
4362
  function parseArgs2(argv) {
4331
4363
  const positional = [];
4332
4364
  const unknown = [];
@@ -4425,9 +4457,8 @@ async function runDoctor(argv) {
4425
4457
  const path = opts.path ?? DEFAULT_PATH;
4426
4458
  try {
4427
4459
  if (!await pathExists(path)) {
4428
- const hint = opts.path === void 0 && await pathExists(CONFIG_FILE) ? ` Run \`saasaloy doctor .\` to check this project instead.` : "";
4429
4460
  cancel2(
4430
- `No such path: ${path} \u2014 point \`doctor\` at a module folder or at a directory of them.${hint}`
4461
+ `No such path: ${path} \u2014 point \`doctor\` at a module folder or at a directory of them.`
4431
4462
  );
4432
4463
  return EXIT_REFUSED;
4433
4464
  }
@@ -6244,6 +6275,7 @@ function baseLabel(version2, hash) {
6244
6275
  }
6245
6276
  function compareBase(args) {
6246
6277
  const { lock, manifest, runningHash, runningVersion } = args;
6278
+ const missing = args.missingTargets ?? [];
6247
6279
  const latest = baseLabel(runningVersion, runningHash);
6248
6280
  if (!isBaseTracked(lock, manifest) || !lock.base) {
6249
6281
  return {
@@ -6256,13 +6288,18 @@ function compareBase(args) {
6256
6288
  detail: "not recorded \u2014 run `saasaloy update` to adopt the base at this CLI"
6257
6289
  };
6258
6290
  }
6291
+ const moved = lock.base.templateHash !== runningHash;
6259
6292
  return {
6260
6293
  name: BASE_MODULE,
6261
6294
  source: BASE_SOURCE,
6262
6295
  ref: BASE_REF,
6263
6296
  current: baseLabel(lock.base.cliVersion, lock.base.templateHash),
6264
6297
  latest,
6265
- status: lock.base.templateHash === runningHash ? "current" : "outdated"
6298
+ status: moved || missing.length > 0 ? "outdated" : "current",
6299
+ // Say why, or an unmoved hash beside `outdated` reads as a bug.
6300
+ ...!moved && missing.length > 0 ? {
6301
+ detail: `${missing.length} tracked file${missing.length === 1 ? " is" : "s are"} missing from disk`
6302
+ } : {}
6266
6303
  };
6267
6304
  }
6268
6305
  function recordRefRewrites(lock, comparisons) {
@@ -6923,11 +6960,13 @@ async function runOutdated(argv) {
6923
6960
  `${REGISTRY_ENV} is set, so every module reads as local \u2014 unset it to compare against the registry.`
6924
6961
  );
6925
6962
  }
6963
+ const templateDir = await baseTemplateDir();
6926
6964
  const baseRow = compareBase({
6927
6965
  lock,
6928
6966
  manifest,
6929
- runningHash: await templateHash(await baseTemplateDir()),
6930
- runningVersion: await readVersion()
6967
+ runningHash: await templateHash(templateDir),
6968
+ runningVersion: await readVersion(),
6969
+ missingTargets: await missingBaseTargets(root, manifest, templateDir)
6931
6970
  });
6932
6971
  const resolvers = /* @__PURE__ */ new Map();
6933
6972
  const remote = (slug, ref) => {
@@ -7953,7 +7992,8 @@ async function runUpdate(argv) {
7953
7992
  lock,
7954
7993
  manifest,
7955
7994
  runningHash: await templateHash(templateDir),
7956
- runningVersion: cliVersion
7995
+ runningVersion: cliVersion,
7996
+ missingTargets: await missingBaseTargets(root, manifest, templateDir)
7957
7997
  });
7958
7998
  }
7959
7999
  if (targets.length === 0 && !baseComparison) {