saasaloy 0.1.0 → 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 +154 -0
- package/dist/index.js +55 -15
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<!-- Generated by scripts/build-cli-readme.ts from the repo root README.md. Edit that file. -->
|
|
2
|
+
|
|
3
|
+
# Saasaloy
|
|
4
|
+
|
|
5
|
+
**Open source, composable SaaS starter kit for Cloudflare.** A CLI plus a module registry, not a boilerplate. Think shadcn/ui for a full-stack SaaS: you scaffold a small base, then copy in the API, database, auth, and product features one command at a time, as source files you own.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/saasaloy)
|
|
8
|
+
[](https://github.com/mimukit/saasaloy/actions/workflows/ci.yml)
|
|
9
|
+
[](LICENSE.md)
|
|
10
|
+
|
|
11
|
+
## Why Saasaloy
|
|
12
|
+
|
|
13
|
+
- **You own the code.** Nothing is imported from a Saasaloy package at runtime. The CLI writes files into your repo and gets out of the way.
|
|
14
|
+
- **Start small, add on demand.** `saasaloy init` gives you a landing page and a UI package. API, database, auth, admin, email, SMS, and features arrive later with `saasaloy add`, each with its dependencies resolved.
|
|
15
|
+
- **Cloudflare-native, near zero cost.** Workers, D1, and static assets by default. Most modules run on the free tier, and the ones that do not say so up front.
|
|
16
|
+
- **Agent-native.** Every generated project ships `AGENTS.md`, `CLAUDE.md`, a `DESIGN.md` contract, and per-module skills for Claude Code and other agents.
|
|
17
|
+
- **Reversible.** `saasaloy remove` undoes a module from its manifest, and `saasaloy update` re-applies at a newer version with a merge plan for your edits.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
Requires Node 24.13.0+ and pnpm 11+. No Cloudflare account is needed until you deploy.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g saasaloy # or: pnpm add -g saasaloy, or prefix commands with npx
|
|
25
|
+
saasaloy init my-app
|
|
26
|
+
cd my-app
|
|
27
|
+
pnpm install
|
|
28
|
+
pnpm dev # landing page on http://localhost:3000
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then compose the product:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
saasaloy list # what the registry offers, and what you already have
|
|
35
|
+
saasaloy add database-d1 # pulls api + database, then binds them to D1 (or pick database-postgres)
|
|
36
|
+
saasaloy add admin # pulls auth, then an auth-gated admin SPA
|
|
37
|
+
saasaloy add waitlist # a feature: form, API route, table
|
|
38
|
+
saasaloy env # fill in the variables your modules declare (--check gates a deploy)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Full walkthrough: [Getting started](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/getting-started.md), then [Make the project yours](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/how-to/make-it-yours.md) for the bundled skills that write your product brief, landing copy, and theme.
|
|
42
|
+
|
|
43
|
+
## What you get
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
my-app/
|
|
47
|
+
apps/web/ Astro landing page (the base)
|
|
48
|
+
apps/api/ Hono Worker (saasaloy add api)
|
|
49
|
+
apps/admin/ TanStack Router SPA (saasaloy add admin)
|
|
50
|
+
packages/ui/ shared React + Tailwind components
|
|
51
|
+
packages/db/ Drizzle schema + client (saasaloy add database-d1 | database-postgres)
|
|
52
|
+
packages/auth/ Better Auth (saasaloy add auth)
|
|
53
|
+
packages/email/ email provider interface (saasaloy add email)
|
|
54
|
+
packages/queue/ background jobs + schedules (saasaloy add queue)
|
|
55
|
+
.agents/skills/ agent skills, symlinked from .claude/skills/
|
|
56
|
+
DESIGN.md the design contract
|
|
57
|
+
saasaloy.json installed modules + alias map
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
| Concern | Choice |
|
|
61
|
+
|---|---|
|
|
62
|
+
| Marketing site (`apps/web`) | Astro on Workers static assets |
|
|
63
|
+
| App (`apps/admin`) | TanStack Router + Vite SPA |
|
|
64
|
+
| Backend (`apps/api`) | Hono on Cloudflare Workers |
|
|
65
|
+
| Database | Drizzle ORM on D1 (SQLite) or Postgres |
|
|
66
|
+
| Auth | Better Auth |
|
|
67
|
+
| Email | Cloudflare Email Sending, Plunk, or a console logger |
|
|
68
|
+
| Background work | Cloudflare Queues and Cron Triggers, or an in-process runner |
|
|
69
|
+
| Infra | wrangler per workspace, or Pulumi via the `infra` module |
|
|
70
|
+
| Monorepo | Turborepo + pnpm |
|
|
71
|
+
|
|
72
|
+
## Commands
|
|
73
|
+
|
|
74
|
+
| Command | What it does |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `init` | scaffold a new project (Astro landing + ui + config) |
|
|
77
|
+
| `add` | apply a module into the current project, resolving `dependsOn` |
|
|
78
|
+
| `env` | fill in the environment variables the installed modules declare (`--check` gates a deploy) |
|
|
79
|
+
| `outdated` | report the base template and each installed module, current vs latest (`--check` gates CI) |
|
|
80
|
+
| `update` | re-apply the base and modules at a newer version, with a merge plan for anything you edited |
|
|
81
|
+
| `remove` | undo a module's applied files via the manifest, offline |
|
|
82
|
+
| `list` | list the modules a registry offers, marking the ones installed here |
|
|
83
|
+
| `new` | scaffold a new module in a registry repo (descriptor + files + skill stub) |
|
|
84
|
+
| `doctor` | validate module descriptors, or a project's state files against each other |
|
|
85
|
+
|
|
86
|
+
Every command answers `--help`. Flags, exit codes, coordinate grammar, and environment variables are in the [Reference](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/reference.md).
|
|
87
|
+
|
|
88
|
+
## Modules
|
|
89
|
+
|
|
90
|
+
Modules come in tiers. A **capability** scaffolds a workspace and sets conventions. A **feature** drops files into those conventions. A **provider** supplies one implementation behind a capability's interface. A **driver** supplies the connection half of a stateful capability, and only one may be installed.
|
|
91
|
+
|
|
92
|
+
| Tier | Modules |
|
|
93
|
+
|---|---|
|
|
94
|
+
| Capability | `api`, `database`, `validators`, `logger`, `auth`, `admin`, `email`, `sms`, `queue`, `infra` |
|
|
95
|
+
| Feature | `waitlist`, `teams`, `email-react` |
|
|
96
|
+
| Provider | `email-console`, `email-cloudflare`, `email-plunk`, `logger-console`, `sms-console`, `queue-cloudflare`, `queue-memory` |
|
|
97
|
+
| Driver | `database-d1`, `database-postgres` |
|
|
98
|
+
|
|
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.
|
|
100
|
+
|
|
101
|
+
The default registry is this repo. `saasaloy add waitlist` fetches `modules/waitlist/` from GitHub at a pinned commit SHA. Any repo with a `modules/` directory can serve as a registry with `saasaloy add owner/repo/<name>`. To publish your own, start at [Contribute a module](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/how-to/contribute-a-module.md).
|
|
102
|
+
|
|
103
|
+
## Cost and requirements
|
|
104
|
+
|
|
105
|
+
`init`, `api`, `database` + `database-d1`, `validators`, `logger`, `auth`, `admin`, `waitlist`, and `teams` all run on Cloudflare's free tier. Cloudflare's limits are Cloudflare's to change, and a project that grows past them should expect to pay.
|
|
106
|
+
|
|
107
|
+
A few modules need something the free tier does not cover:
|
|
108
|
+
|
|
109
|
+
| Module | Needs |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `email-cloudflare` | a Workers paid plan and a sending domain onboarded by hand in the Cloudflare dashboard |
|
|
112
|
+
| `email-plunk` | a [Plunk](https://www.useplunk.com) account and `PLUNK_API_KEY` |
|
|
113
|
+
| `database-postgres` | a Postgres server reachable from a Worker, with its URL in `DATABASE_URL`. Install instead of `database-d1`, never alongside |
|
|
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 |
|
|
116
|
+
|
|
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).
|
|
118
|
+
|
|
119
|
+
## Deploy
|
|
120
|
+
|
|
121
|
+
Each deployable workspace owns its `wrangler.jsonc` and `deploy` script. Run them one at a time, or install the `infra` module and deploy every Worker with one Pulumi program. See [Deploy to Cloudflare](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/how-to/deploy-to-cloudflare.md).
|
|
122
|
+
|
|
123
|
+
## Documentation
|
|
124
|
+
|
|
125
|
+
All docs live in [`docs/wiki/`](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/index.md).
|
|
126
|
+
|
|
127
|
+
**Use Saasaloy**
|
|
128
|
+
|
|
129
|
+
- [Getting started](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/getting-started.md): install the CLI, scaffold a project, run it.
|
|
130
|
+
- [Make the project yours](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/how-to/make-it-yours.md): the bundled skills for the product brief, landing copy, and theme.
|
|
131
|
+
- [Modules](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/modules.md): every module in the default registry, in one table.
|
|
132
|
+
- [Add a module](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/how-to/add-a-module.md): install a feature and its prerequisites.
|
|
133
|
+
- [Remove a module](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/how-to/remove-a-module.md): take one back out, and what stays behind.
|
|
134
|
+
- [Deploy to Cloudflare](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/how-to/deploy-to-cloudflare.md): ship each workspace.
|
|
135
|
+
|
|
136
|
+
**Build a module**
|
|
137
|
+
|
|
138
|
+
- [Contribute a module](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/how-to/contribute-a-module.md): authoring guides and how to test a module before it ships.
|
|
139
|
+
- [A bad descriptor reached `main`](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/runbooks/bad-descriptor-on-main.md): the registry is live, so this is an incident.
|
|
140
|
+
|
|
141
|
+
**Both tracks**
|
|
142
|
+
|
|
143
|
+
- [Architecture](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/architecture.md): how the CLI, the registry, and a generated project fit together.
|
|
144
|
+
- [Reference](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/reference.md): every command, flag, environment variable, and config file.
|
|
145
|
+
- [`CONTEXT.md`](https://github.com/mimukit/saasaloy/blob/main/CONTEXT.md): the vocabulary. Module, capability, provider, coordinate, applier.
|
|
146
|
+
- [`docs/adr/`](https://github.com/mimukit/saasaloy/blob/main/docs/adr/): why the design is what it is, one decision per file.
|
|
147
|
+
|
|
148
|
+
## Contributing
|
|
149
|
+
|
|
150
|
+
Issues and pull requests are welcome. [`CONTRIBUTING.md`](https://github.com/mimukit/saasaloy/blob/main/CONTRIBUTING.md) covers the `.dev/playground`, the lint and test gates, and the dependency update flow. The `billing` module is tracked in [#14](https://github.com/mimukit/saasaloy/issues/14).
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
[MIT](https://github.com/mimukit/saasaloy/blob/main/LICENSE.md)
|
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
|
|
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
|
-
[
|
|
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,
|
|
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
|
|
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 ?
|
|
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(
|
|
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.
|
|
3689
|
+
version: "0.1.2",
|
|
3671
3690
|
description: "Composable SaaS accelerator kit.",
|
|
3672
3691
|
keywords: [
|
|
3673
3692
|
"saas",
|
|
@@ -3706,7 +3725,7 @@ var package_default = {
|
|
|
3706
3725
|
},
|
|
3707
3726
|
scripts: {
|
|
3708
3727
|
build: "tsup",
|
|
3709
|
-
prepack: "pnpm run build",
|
|
3728
|
+
prepack: "pnpm -w run readme:cli && pnpm run build",
|
|
3710
3729
|
dev: "tsup --watch",
|
|
3711
3730
|
start: "node ./dist/index.js",
|
|
3712
3731
|
typecheck: "tsc --noEmit",
|
|
@@ -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 = "
|
|
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
|
|
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:
|
|
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(
|
|
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) {
|