@stacksjs/actions 0.70.162 → 0.70.164

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
@@ -49,7 +49,7 @@ For help, discussion about best practices, or any other conversation that would
49
49
 
50
50
  For casual chit-chat with others using this package:
51
51
 
52
- [Join the Stacks Discord Server](https://discord.gg/stacksjs)
52
+ [Join the Stacks Discord Server](https://stacksjs.com/discord)
53
53
 
54
54
  ## 📄 License
55
55
 
package/dist/bump.js CHANGED
@@ -138,6 +138,11 @@ if (!isDryRun && existsSync(p.projectPath("bun.lock"))) {
138
138
  writeFileSync(lockPath, previousLock);
139
139
  throw error;
140
140
  }
141
+ const expectedLockfileVersion = 1, versionMatch = readFileSync(lockPath, "utf8").match(/"lockfileVersion"\s*:\s*(\d+)/), producedVersion = versionMatch ? Number(versionMatch[1]) : null;
142
+ if (producedVersion !== expectedLockfileVersion) {
143
+ writeFileSync(lockPath, previousLock);
144
+ throw Error(`Release aborted: regenerating bun.lock produced lockfileVersion ${producedVersion ?? "unknown"}, but CI's Bun (1.3.x) requires v${expectedLockfileVersion}. You are releasing with a newer Bun (1.4.x writes v2). Re-run the release with Bun 1.3.x (e.g. via \`bunx bun@1.3.14\`) so CI can parse the lockfile.`);
145
+ }
141
146
  }
142
147
  function pinMetaCoreDeps(version) {
143
148
  const metaPath = p.frameworkPath("core/package.json"), meta = JSON.parse(readFileSync(metaPath, "utf-8"));
@@ -1014,7 +1014,7 @@ SVCEOF`,
1014
1014
  } else
1015
1015
  smtpSpinner.fail(`No Pantry-provided ${MAIL_TARGET_PLATFORM} mail binary found. Release ${MAIL_PACKAGE_DOMAIN}, then run the Pantry binary sync for that package.`);
1016
1016
  } else {
1017
- const smtpServerSrc = resolvePath(p.projectPath("storage/framework/core/cloud/src/imap/smtp-server.ts")), bundleOutDir = joinPath(p.projectPath(), ".stacks/tmp/smtp-bundle");
1017
+ const smtpServerSrc = resolvePath(p.projectPath("storage/framework/core/cloud/src/imap/smtp-server.ts")), bundleOutDir = p.frameworkRuntimePath("tmp/smtp-bundle");
1018
1018
  execSync(`bun build ${smtpServerSrc} --target=bun --outdir=${bundleOutDir}`, { stdio: "pipe" });
1019
1019
  const bundlePath = joinPath(bundleOutDir, "smtp-server.js");
1020
1020
  if (smtpFileExists(bundlePath)) {
@@ -323,7 +323,7 @@ if (!proxyStarted)
323
323
  console.warn("[Dashboard] Reverse proxy failed:", err);
324
324
  return !1;
325
325
  });
326
- const dashboardHttpsUrl = dashboardDomain ? `https://${dashboardDomain}` : null, dashboardLocalUrl = `http://localhost:${dashboardPort}`, initialUrl = dashboardHttpsUrl && proxyStarted ? `${dashboardHttpsUrl}/` : `${dashboardLocalUrl}/`, elapsedMs = (Bun.nanoseconds() - startTime) / 1e6;
326
+ const dashboardHttpsUrl = dashboardDomain ? `https://${dashboardDomain}` : null, dashboardLocalUrl = `http://localhost:${dashboardPort}`, initialUrl = dashboardHttpsUrl && proxyStarted ? `${dashboardHttpsUrl}/` : `${dashboardLocalUrl}/`, nativeWindowUrl = `${dashboardLocalUrl}/`, elapsedMs = (Bun.nanoseconds() - startTime) / 1e6;
327
327
  console.log();
328
328
  console.log(` ${bold(cyan("stacks dashboard"))}`);
329
329
  console.log();
@@ -396,7 +396,7 @@ if (createApp) {
396
396
  await new Promise(() => {});
397
397
  }
398
398
  const app = createApp({
399
- url: initialUrl,
399
+ url: nativeWindowUrl,
400
400
  quiet: !verbose,
401
401
  ...craftBinaryPath && { craftPath: craftBinaryPath },
402
402
  window: {
@@ -32,5 +32,13 @@ export declare function generateTypes(options?: GeneratorOptions): Promise<void>
32
32
  export declare function watchTypes(options?: GeneratorOptions): Promise<void>;
33
33
  export declare function generatePantryConfig(): void;
34
34
  export declare function generateSeeder(): Promise<void>;
35
+ /**
36
+ * Drops a `.framework` symlink in the project root pointing at
37
+ * `storage/framework`, so core developers can `cd .framework` instead of typing
38
+ * the full path. Purely a convenience; nothing in the framework depends on it.
39
+ *
40
+ * It used to be called `.stacks`, which now collides with the pre-relocation
41
+ * name of the runtime scratch directory (see `ensureRuntimeDirectories`).
42
+ */
35
43
  export declare function generateCoreSymlink(): Promise<void>;
36
44
  export declare function generateOpenApiSpec(): Promise<void>;
@@ -149,7 +149,10 @@ export function generatePantryConfig() {
149
149
  }
150
150
  export async function generateSeeder() {}
151
151
  export async function generateCoreSymlink() {
152
- await runCommand(`ln -s ${frameworkPath()} ${projectPath(".stacks")}`);
152
+ const link = projectPath(".framework");
153
+ if (fs.existsSync(link))
154
+ await runCommand(`rm -f ${link}`);
155
+ await runCommand(`ln -s ${frameworkPath()} ${link}`);
153
156
  }
154
157
  export async function generateOpenApiSpec() {
155
158
  const { generateOpenApi } = await import("@stacksjs/api");
@@ -29,8 +29,10 @@ export function publishedActionCandidates(action, packageRoot) {
29
29
  export function developmentConditionForProject(projectRoot) {
30
30
  return existsSync(join(projectRoot, "storage/framework/core")) && existsSync(join(projectRoot, "node_modules/@stacksjs/env/src/index.ts")) ? "--conditions development" : "";
31
31
  }
32
- async function resolveActionFile(action) {
32
+ async function resolveActionFile(action, projectRoot) {
33
33
  const candidates = [];
34
+ if (projectRoot)
35
+ candidates.push(join(projectRoot, `storage/framework/core/actions/src/${action}.ts`));
34
36
  candidates.push(p.actionsPath(`src/${action}.ts`));
35
37
  candidates.push(...publishedActionCandidates(action));
36
38
  for (const candidate of candidates)
@@ -60,8 +62,9 @@ export async function runAction(action, options) {
60
62
  } catch (error) {
61
63
  return err(`Failed to start dev server: ${error}`);
62
64
  }
63
- if (!(action.includes("/") || ["dev", "build", "install", "upgrade", "migrate"].some((prefix) => action.startsWith(prefix)))) {
64
- const glob = new Bun.Glob("**/*.{ts,js}"), scanOptions = { cwd: p.userActionsPath(), onlyFiles: !0, absolute: !0 }, matchingFiles = [], basePath = p.userActionsPath();
65
+ const isLikelyCoreAction = action.includes("/") || ["dev", "build", "install", "upgrade", "migrate"].some((prefix) => action.startsWith(prefix)), userActionsBase = options?.cwd ? join(String(options.cwd), "app/Actions") : p.userActionsPath();
66
+ if (!isLikelyCoreAction && existsSync(userActionsBase)) {
67
+ const glob = new Bun.Glob("**/*.{ts,js}"), scanOptions = { cwd: userActionsBase, onlyFiles: !0, absolute: !0 }, matchingFiles = [], basePath = userActionsBase;
65
68
  for await (const file of glob.scan(scanOptions)) {
66
69
  if (file.replace(`${basePath}/`, "").replace(/\.(ts|js)$/, "") === action || file.endsWith(`${action}.ts`) || file.endsWith(`${action}.js`)) {
67
70
  log.debug(`[action] Resolved: ${action} \u2192 ${file}`);
@@ -78,7 +81,7 @@ export async function runAction(action, options) {
78
81
  }
79
82
  } catch (error) {}
80
83
  }
81
- const path = await resolveActionFile(action);
84
+ const path = await resolveActionFile(action, options?.cwd ? String(options.cwd) : void 0);
82
85
  if (!path)
83
86
  return err(`Action '${action}' not found in storage/framework/core/actions/src or @stacksjs/actions`);
84
87
  log.debug(`[action] Resolved: ${action} \u2192 ${path}`);
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Every dependency name a manifest pins to `catalog:`.
3
+ *
4
+ * Bun also accepts the named form (`catalog:react19`); those resolve from a
5
+ * separate `catalogs` map rather than the default catalog, so we deliberately
6
+ * match only the bare default protocol here.
7
+ */
8
+ export declare function collectCatalogReferences(manifests: PackageManifest[]): string[];
9
+ /**
10
+ * Read a catalog off a manifest, accepting either shape Bun supports: a
11
+ * top-level `catalog` field, or `workspaces.catalog` on the object form.
12
+ */
13
+ export declare function readCatalogField(manifest: PackageManifest | null | undefined): Record<string, string>;
14
+ /**
15
+ * Merge the upstream catalog into the app's own, restricted to what the
16
+ * vendored tree references.
17
+ *
18
+ * User-authored entries survive untouched: we only write keys that appear in
19
+ * `referenced`, so a catalog entry the app added for its own packages is never
20
+ * clobbered by an upgrade.
21
+ */
22
+ export declare function mergeCatalog(current: Record<string, string>, upstream: Record<string, string>, referenced: string[]): CatalogMergeResult;
23
+ /** Parse a `package.json`, returning null rather than throwing on bad input. */
24
+ export declare function readManifest(path: string): PackageManifest | null;
25
+ /**
26
+ * Walk a synced tree for workspace manifests. Depth is bounded because the
27
+ * framework nests packages at most a few levels deep, and an unbounded walk
28
+ * over a vendored tree is needlessly expensive on every upgrade.
29
+ */
30
+ export declare function findWorkspaceManifests(root: string, maxDepth?: number): string[];
31
+ /**
32
+ * Fetch the upstream catalog for a GitHub-sourced upgrade.
33
+ *
34
+ * We hit the raw root manifest directly rather than re-downloading a tarball
35
+ * we've already extracted the interesting parts of. Failure is non-fatal: the
36
+ * caller falls back to leaving the existing catalog alone.
37
+ */
38
+ export declare function fetchUpstreamCatalog(ref: string): Promise<Record<string, string>>;
39
+ /**
40
+ * Mirror the upstream catalog into the app's root manifest so the vendored
41
+ * workspace members resolve. Returns null when there is nothing to do.
42
+ */
43
+ export declare function reconcileWorkspaceCatalog(options: ReconcileCatalogOptions): Promise<CatalogMergeResult | null>;
44
+ export declare interface PackageManifest {
45
+ catalog?: Record<string, string>
46
+ workspaces?: string[] | { catalog?: Record<string, string> }
47
+ [field: string]: unknown
48
+ }
49
+ export declare interface CatalogMergeResult {
50
+ catalog: Record<string, string>
51
+ added: string[]
52
+ updated: string[]
53
+ missing: string[]
54
+ }
55
+ export declare interface ReconcileCatalogOptions {
56
+ projectRoot: string
57
+ frameworkRoot: string
58
+ localStacksRoot?: string | null
59
+ ref?: string
60
+ }
@@ -0,0 +1,113 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const CATALOG_PROTOCOL = "catalog:", DEPENDENCY_FIELDS = [
4
+ "dependencies",
5
+ "devDependencies",
6
+ "peerDependencies",
7
+ "optionalDependencies"
8
+ ], MANIFEST_SCAN_SKIP = new Set(["node_modules", "dist", ".git", ".cache", "cache"]);
9
+ export function collectCatalogReferences(manifests) {
10
+ const names = new Set;
11
+ for (const manifest of manifests)
12
+ for (const field of DEPENDENCY_FIELDS) {
13
+ const deps = manifest[field];
14
+ if (!deps || typeof deps !== "object")
15
+ continue;
16
+ for (const [name, range] of Object.entries(deps))
17
+ if (range === CATALOG_PROTOCOL)
18
+ names.add(name);
19
+ }
20
+ return [...names].sort();
21
+ }
22
+ export function readCatalogField(manifest) {
23
+ if (!manifest)
24
+ return {};
25
+ if (manifest.catalog && typeof manifest.catalog === "object")
26
+ return { ...manifest.catalog };
27
+ const workspaces = manifest.workspaces;
28
+ if (workspaces && !Array.isArray(workspaces) && workspaces.catalog)
29
+ return { ...workspaces.catalog };
30
+ return {};
31
+ }
32
+ export function mergeCatalog(current, upstream, referenced) {
33
+ const catalog = { ...current }, added = [], updated = [], missing = [];
34
+ for (const name of referenced) {
35
+ const version = upstream[name];
36
+ if (!version) {
37
+ if (!(name in catalog))
38
+ missing.push(name);
39
+ continue;
40
+ }
41
+ if (!(name in catalog)) {
42
+ catalog[name] = version;
43
+ added.push(name);
44
+ continue;
45
+ }
46
+ if (catalog[name] !== version) {
47
+ catalog[name] = version;
48
+ updated.push(name);
49
+ }
50
+ }
51
+ return { catalog: sortKeys(catalog), added, updated, missing };
52
+ }
53
+ function sortKeys(record) {
54
+ return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
55
+ }
56
+ export function readManifest(path) {
57
+ try {
58
+ if (!existsSync(path))
59
+ return null;
60
+ return JSON.parse(readFileSync(path, "utf-8"));
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+ export function findWorkspaceManifests(root, maxDepth = 4) {
66
+ const found = [], walk = (dir, depth) => {
67
+ if (depth > maxDepth || !existsSync(dir))
68
+ return;
69
+ const manifest = join(dir, "package.json");
70
+ if (existsSync(manifest))
71
+ found.push(manifest);
72
+ let entries;
73
+ try {
74
+ entries = readdirSync(dir, { withFileTypes: !0 });
75
+ } catch {
76
+ return;
77
+ }
78
+ for (const entry of entries) {
79
+ if (!entry.isDirectory())
80
+ continue;
81
+ if (entry.name.startsWith(".") || MANIFEST_SCAN_SKIP.has(entry.name))
82
+ continue;
83
+ walk(join(dir, entry.name), depth + 1);
84
+ }
85
+ };
86
+ walk(root, 0);
87
+ return found;
88
+ }
89
+ export async function fetchUpstreamCatalog(ref) {
90
+ try {
91
+ const response = await fetch(`https://raw.githubusercontent.com/stacksjs/stacks/${ref}/package.json`);
92
+ if (!response.ok)
93
+ return {};
94
+ return readCatalogField(await response.json());
95
+ } catch {
96
+ return {};
97
+ }
98
+ }
99
+ export async function reconcileWorkspaceCatalog(options) {
100
+ const rootManifestPath = join(options.projectRoot, "package.json"), rootManifest = readManifest(rootManifestPath);
101
+ if (!rootManifest)
102
+ return null;
103
+ const manifests = findWorkspaceManifests(options.frameworkRoot).map(readManifest).filter((manifest) => manifest !== null), referenced = collectCatalogReferences(manifests);
104
+ if (referenced.length === 0)
105
+ return null;
106
+ const upstream = options.localStacksRoot ? readCatalogField(readManifest(join(options.localStacksRoot, "package.json"))) : await fetchUpstreamCatalog(options.ref || "main"), result = mergeCatalog(readCatalogField(rootManifest), upstream, referenced);
107
+ if (result.added.length === 0 && result.updated.length === 0)
108
+ return result;
109
+ rootManifest.catalog = result.catalog;
110
+ await Bun.write(rootManifestPath, `${JSON.stringify(rootManifest, null, 2)}
111
+ `);
112
+ return result;
113
+ }
@@ -1,3 +1,27 @@
1
+ /**
2
+ * A `pantry.lock` is the marker: it only exists once pantry owns the graph.
3
+ *
4
+ * Getting this wrong is not cosmetic. Running `bun install` in a pantry app
5
+ * refreshes a `node_modules` tree that nothing resolves against, so the
6
+ * upgrade reports success while the app keeps importing the stale copies in
7
+ * `pantry/` - which surfaces later as missing exports from packages the
8
+ * manifest claims are current.
9
+ */
10
+ export declare function detectDependencyInstaller(hasPantryLock: boolean): DependencyInstaller;
11
+ /**
12
+ * Build the refresh argv. Both installers spell the "re-resolve everything"
13
+ * flag `--force`, so the shape is the same either way.
14
+ */
15
+ export declare function resolveDependencyRefreshCommand(options: {
16
+ installer: DependencyInstaller
17
+ bunExecutable: string
18
+ pantryExecutable: string
19
+ }): string[];
20
+ /**
21
+ * Pick the first pantry binary that exists, falling back to a bare `pantry`
22
+ * so a PATH install still works. `exists` is injected to keep this testable.
23
+ */
24
+ export declare function resolvePantryExecutable(candidates: string[], exists: (path: string) => boolean): string;
1
25
  /**
2
26
  * The first upgrade process records the real file changes, then restarts itself
3
27
  * after replacing its own framework code. The restarted process sees the
@@ -14,8 +38,8 @@ export declare function shouldRefreshPostSyncDependencies(corePackageChanged: bo
14
38
  /**
15
39
  * Fully refresh dependencies after replacing vendored workspace manifests.
16
40
  *
17
- * A plain `bun install` may reuse the existing nested package placement and
18
- * leave a lockfile that changes in a clean checkout. `--force` makes Bun
41
+ * A plain install may reuse the existing nested package placement and leave a
42
+ * lockfile that changes in a clean checkout. `--force` makes the installer
19
43
  * resolve the complete workspace graph so the resulting lockfile is valid for
20
44
  * subsequent frozen installs.
21
45
  */
@@ -45,8 +69,17 @@ declare interface PostSyncMigrationOptions {
45
69
  spawn: PostSyncSpawn
46
70
  }
47
71
  declare interface PostSyncDependencyOptions {
48
- bunExecutable: string
72
+ cmd: string[]
49
73
  projectRoot: string
50
74
  spawn: PostSyncSpawn
51
75
  }
52
76
  declare type PostSyncSpawn = (options: PostSyncSpawnOptions) => { exited: Promise<number> }
77
+ /**
78
+ * Which installer owns the project's dependency graph.
79
+ *
80
+ * Stacks apps come in two flavours. The default resolves third-party packages
81
+ * from `node_modules` and is installed by Bun. A pantry app instead installs
82
+ * into `pantry/` and points tsconfig's catch-all path mapping at it, so that
83
+ * directory - not `node_modules` - is what module resolution actually reads.
84
+ */
85
+ export type DependencyInstaller = 'bun' | 'pantry';
@@ -1,3 +1,15 @@
1
+ export function detectDependencyInstaller(hasPantryLock) {
2
+ return hasPantryLock ? "pantry" : "bun";
3
+ }
4
+ export function resolveDependencyRefreshCommand(options) {
5
+ return [options.installer === "pantry" ? options.pantryExecutable : options.bunExecutable, "install", "--force"];
6
+ }
7
+ export function resolvePantryExecutable(candidates, exists) {
8
+ for (const candidate of candidates)
9
+ if (candidate && exists(candidate))
10
+ return candidate;
11
+ return "pantry";
12
+ }
1
13
  export function shouldRunPostSyncHooks(changeCount, alreadyRestarted) {
2
14
  return changeCount > 0 || alreadyRestarted;
3
15
  }
@@ -6,7 +18,7 @@ export function shouldRefreshPostSyncDependencies(corePackageChanged, alreadyRes
6
18
  }
7
19
  export async function runPostSyncDependencyRefresh(options) {
8
20
  const code = await options.spawn({
9
- cmd: [options.bunExecutable, "install", "--force"],
21
+ cmd: options.cmd,
10
22
  cwd: options.projectRoot,
11
23
  stdin: "ignore",
12
24
  stdout: "inherit",
@@ -2,17 +2,35 @@
2
2
  * Resolve the target channel and git ref from CLI options and persisted channel.
3
3
  * Priority: --version > --canary > --stable > persisted channel
4
4
  *
5
- * Branch model: `main` is the bleeding-edge "dump everything" branch; `stable`
6
- * is the vetted branch that PRs are merged into from `main` once main is proven.
7
- * So the `stable` channel tracks the `stable` branch (vetted) and the `canary`
8
- * channel tracks the `main` branch (bleeding edge). A pinned `--version` still
9
- * resolves to the matching `vX` tag.
5
+ * Release model: `main` is the bleeding-edge "dump everything" branch, and each
6
+ * release is cut as a `vX.Y.Z` git tag (see `.github/workflows/release.yml`,
7
+ * which triggers on `push: tags: ['v*']`). There is deliberately no long-lived
8
+ * `stable` branch. So the `canary` channel tracks the `main` branch (bleeding
9
+ * edge) and the `stable` channel tracks the latest published `vX.Y.Z` tag
10
+ * passed in as `latestStableRef` because resolving it needs the network, which
11
+ * this pure function stays free of. A pinned `--version` resolves directly to
12
+ * the matching `vX` tag.
10
13
  */
11
14
  export declare function resolveUpgradeContext(options: {
12
15
  version?: string
13
16
  canary?: boolean
14
17
  stable?: boolean
15
- }, currentChannel: 'stable' | 'canary'): UpgradeContext;
18
+ }, currentChannel: 'stable' | 'canary', latestStableRef: string): UpgradeContext;
19
+ /**
20
+ * Pick the highest stable release tag from a list of tag names. Only clean
21
+ * `vMAJOR.MINOR.PATCH` tags qualify — pre-releases (`v1.0.0-beta.1`), annotated
22
+ * deref entries (`…^{}`), and non-version tags are ignored. Returns null when
23
+ * nothing qualifies.
24
+ */
25
+ export declare function pickLatestStableTag(tags: string[]): string | null;
26
+ /**
27
+ * Resolve the latest published stable release tag (e.g. `"v0.70.163"`) by
28
+ * listing the remote's `v*` tags. The stable channel installs this tag rather
29
+ * than a branch, because releases are cut as tags and no `stable` branch
30
+ * exists. Returns null if the tags can't be listed (offline / no git), letting
31
+ * the caller fall back to the currently-installed version.
32
+ */
33
+ export declare function resolveLatestStableTag(): Promise<string | null>;
16
34
  /**
17
35
  * Build the gitit template string for a path under the stacks repo.
18
36
  * `subPath` is relative to the repo root (e.g. `storage/framework/core`).
@@ -2,17 +2,49 @@ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import process from "node:process";
5
- export function resolveUpgradeContext(options, currentChannel) {
5
+ export function resolveUpgradeContext(options, currentChannel, latestStableRef) {
6
6
  const targetVersion = options.version;
7
7
  if (targetVersion)
8
8
  return { channel: "stable", ref: targetVersion.startsWith("v") ? targetVersion : `v${targetVersion}`, targetVersion };
9
9
  if (options.canary)
10
10
  return { channel: "canary", ref: "main" };
11
11
  if (options.stable)
12
- return { channel: "stable", ref: "stable" };
12
+ return { channel: "stable", ref: latestStableRef };
13
13
  if (currentChannel === "canary")
14
14
  return { channel: "canary", ref: "main" };
15
- return { channel: "stable", ref: "stable" };
15
+ return { channel: "stable", ref: latestStableRef };
16
+ }
17
+ function compareVersionParts(a, b) {
18
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
19
+ }
20
+ export function pickLatestStableTag(tags) {
21
+ const re = /^v(\d+)\.(\d+)\.(\d+)$/;
22
+ let best = null;
23
+ for (const tag of tags) {
24
+ const m = re.exec(tag.trim());
25
+ if (!m)
26
+ continue;
27
+ const parts = [Number(m[1]), Number(m[2]), Number(m[3])];
28
+ if (!best || compareVersionParts(parts, best.parts) > 0)
29
+ best = { tag: tag.trim(), parts };
30
+ }
31
+ return best?.tag ?? null;
32
+ }
33
+ export async function resolveLatestStableTag() {
34
+ try {
35
+ const proc = Bun.spawn({
36
+ cmd: ["git", "ls-remote", "--tags", "--refs", "https://github.com/stacksjs/stacks.git", "v*"],
37
+ stdout: "pipe",
38
+ stderr: "pipe"
39
+ }), out = await new Response(proc.stdout).text();
40
+ if (await proc.exited !== 0)
41
+ return null;
42
+ const tags = out.split(`
43
+ `).map((line) => line.split("\t")[1]?.replace("refs/tags/", "")).filter((t) => !!t);
44
+ return pickLatestStableTag(tags);
45
+ } catch {
46
+ return null;
47
+ }
16
48
  }
17
49
  export function buildTemplateString(ref, subPath = "storage/framework/core") {
18
50
  return `github:stacksjs/stacks/${subPath}#${ref}`;
@@ -15,6 +15,7 @@ import {
15
15
  readChannel,
16
16
  readSyncedVersion,
17
17
  readVersion,
18
+ resolveLatestStableTag,
18
19
  resolveSuccessMessage,
19
20
  resolveUpgradeContext,
20
21
  resolveUpgradeMessage,
@@ -25,7 +26,11 @@ import {
25
26
  writeChannel,
26
27
  writeSyncedVersion
27
28
  } from "./framework-utils";
29
+ import { reconcileWorkspaceCatalog } from "./catalog";
28
30
  import {
31
+ detectDependencyInstaller,
32
+ resolveDependencyRefreshCommand,
33
+ resolvePantryExecutable,
29
34
  runPostSyncDependencyRefresh,
30
35
  runPostSyncMigration,
31
36
  shouldRefreshPostSyncDependencies,
@@ -43,7 +48,11 @@ if (!existsSync(p.projectPath("storage/framework/core"))) {
43
48
  noPostinstall: options.noPostinstall ?? options.postinstall === !1
44
49
  });
45
50
  }
46
- const channelFile = join(projectRoot, ".stacks-channel"), versionFile = join(projectRoot, ".stacks-version"), currentChannel = readChannel(channelFile), ctx = resolveUpgradeContext(options, currentChannel), corePkgPath = p.projectPath("storage/framework/core/buddy/package.json"), currentVersion = readVersion(corePkgPath), explicitLocal = options.from ? p.projectPath(options.from) : null, detectedLocal = explicitLocal ?? (shouldAutoDetectLocalStacks(options) ? detectLocalStacks(projectRoot) : null), usingLocal = !!detectedLocal;
51
+ const channelFile = join(projectRoot, ".stacks-channel"), versionFile = join(projectRoot, ".stacks-version"), currentChannel = readChannel(channelFile), corePkgPath = p.projectPath("storage/framework/core/buddy/package.json"), currentVersion = readVersion(corePkgPath), willResolveStable = !options.version && !options.canary && (!!options.stable || currentChannel !== "canary");
52
+ let latestStableRef = "";
53
+ if (willResolveStable)
54
+ latestStableRef = await resolveLatestStableTag() ?? (currentVersion ? `v${currentVersion}` : "main");
55
+ const ctx = resolveUpgradeContext(options, currentChannel, latestStableRef), explicitLocal = options.from ? p.projectPath(options.from) : null, detectedLocal = explicitLocal ?? (shouldAutoDetectLocalStacks(options) ? detectLocalStacks(projectRoot) : null), usingLocal = !!detectedLocal;
47
56
  console.log(resolveUpgradeMessage(ctx, currentChannel, !!options.stable));
48
57
  console.log(usingLocal ? ` source: local checkout at ${detectedLocal}` : ` source: github:stacksjs/stacks#${ctx.ref}`);
49
58
  if (options.dryRun) {
@@ -130,7 +139,7 @@ for (const { managed, summary } of perPath)
130
139
  const runHooks = !options.noPostinstall && options.postinstall !== !1;
131
140
  if (runHooks)
132
141
  try {
133
- await runPostSyncHooks({ aggregate, perPath, projectRoot, alreadyRestarted });
142
+ await runPostSyncHooks({ aggregate, perPath, projectRoot, alreadyRestarted, localStacksRoot: detectedLocal, ref: ctx.ref });
134
143
  } catch (err) {
135
144
  console.error(`Post-sync hooks failed: ${err?.message || err}`);
136
145
  process.exit(ExitCode.FatalError);
@@ -303,13 +312,21 @@ async function getRemoteSha(context, fromLocal, stacksRoot) {
303
312
  return await head.exited === 0 && SHA_RE.test(h) ? h.toLowerCase() : null;
304
313
  }
305
314
  const proc = Bun.spawn({
306
- cmd: ["git", "ls-remote", "https://github.com/stacksjs/stacks.git", `refs/heads/${context.ref}`],
315
+ cmd: [
316
+ "git",
317
+ "ls-remote",
318
+ "https://github.com/stacksjs/stacks.git",
319
+ `refs/heads/${context.ref}`,
320
+ `refs/tags/${context.ref}`,
321
+ `refs/tags/${context.ref}^{}`
322
+ ],
307
323
  stdout: "pipe",
308
324
  stderr: "pipe"
309
325
  }), out = await new Response(proc.stdout).text();
310
326
  if (await proc.exited !== 0)
311
327
  return null;
312
- const m = out.match(/^([0-9a-f]{40})\s/i);
328
+ const lines = out.split(`
329
+ `).filter(Boolean), m = (lines.find((l) => l.includes("^{}")) ?? lines[0])?.match(/^([0-9a-f]{40})\s/i);
313
330
  return m?.[1] ? m[1].toLowerCase() : null;
314
331
  } catch {
315
332
  return null;
@@ -400,7 +417,7 @@ async function syncRootFilesFromGitHub(ref) {
400
417
  }
401
418
  }
402
419
  async function runPostSyncHooks(args) {
403
- const { aggregate, perPath, projectRoot, alreadyRestarted } = args, changeCount = aggregate.added + aggregate.changed + aggregate.removed;
420
+ const { aggregate, perPath, projectRoot, alreadyRestarted, localStacksRoot, ref } = args, changeCount = aggregate.added + aggregate.changed + aggregate.removed;
404
421
  if (!shouldRunPostSyncHooks(changeCount, alreadyRestarted)) {
405
422
  console.log("Nothing changed \u2014 skipping post-sync hooks.");
406
423
  return;
@@ -412,6 +429,25 @@ async function runPostSyncHooks(args) {
412
429
  } catch (err) {
413
430
  console.warn(` auto-imports failed (non-fatal): ${err?.message || err}`);
414
431
  }
432
+ console.log("Reconciling workspace dependency catalog...");
433
+ try {
434
+ const result = await reconcileWorkspaceCatalog({
435
+ projectRoot,
436
+ frameworkRoot: join(projectRoot, "storage", "framework"),
437
+ localStacksRoot,
438
+ ref
439
+ });
440
+ if (!result)
441
+ console.log(" catalog: nothing to reconcile");
442
+ else if (result.added.length || result.updated.length)
443
+ console.log(` catalog: +${result.added.length} added, ~${result.updated.length} updated`);
444
+ else
445
+ console.log(" catalog: up to date");
446
+ if (result?.missing.length)
447
+ console.warn(` catalog: no upstream version for ${result.missing.join(", ")}`);
448
+ } catch (err) {
449
+ console.warn(` catalog reconcile failed (non-fatal): ${err?.message || err}`);
450
+ }
415
451
  if (shouldRefreshPostSyncDependencies(perPath.some((entry) => {
416
452
  const { managed, summary } = entry;
417
453
  if (managed.isFile)
@@ -422,15 +458,20 @@ async function runPostSyncHooks(args) {
422
458
  return !1;
423
459
  return pkgJsonInTree(join(projectRoot, managed.localPath));
424
460
  }), alreadyRestarted)) {
425
- console.log("Running `bun install` to refresh dependencies...");
461
+ const installer = detectDependencyInstaller(existsSync(join(projectRoot, "pantry.lock"))), cmd = resolveDependencyRefreshCommand({
462
+ installer,
463
+ bunExecutable: process.argv[0] || "bun",
464
+ pantryExecutable: resolvePantryExecutable(pantryExecutableCandidates(projectRoot), existsSync)
465
+ });
466
+ console.log(`Running \`${installer} install\` to refresh dependencies...`);
426
467
  try {
427
468
  await runPostSyncDependencyRefresh({
428
- bunExecutable: process.argv[0] || "bun",
469
+ cmd,
429
470
  projectRoot,
430
471
  spawn: (spawnOptions) => Bun.spawn(spawnOptions)
431
472
  });
432
473
  } catch (err) {
433
- console.warn(` bun install failed (non-fatal): ${err?.message || err}`);
474
+ console.warn(` ${installer} install failed (non-fatal): ${err?.message || err}`);
434
475
  }
435
476
  }
436
477
  const migrationsDir = join(projectRoot, "database", "migrations");
@@ -445,6 +486,14 @@ async function runPostSyncHooks(args) {
445
486
  });
446
487
  }
447
488
  }
489
+ function pantryExecutableCandidates(projectRoot) {
490
+ const home = process.env.HOME ?? "";
491
+ return [
492
+ join(projectRoot, "pantry", ".bin", "pantry"),
493
+ home ? join(home, ".local", "bin", "pantry") : "",
494
+ home ? join(home, ".local", "share", "pantry", "global", "bin", "pantry") : ""
495
+ ].filter(Boolean);
496
+ }
448
497
  function pkgJsonInTree(dir) {
449
498
  try {
450
499
  if (!existsSync(dir))
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/actions",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.162",
5
+ "version": "0.70.164",
6
6
  "description": "The Stacks actions.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -62,32 +62,32 @@
62
62
  "prepublishOnly": "bun run build"
63
63
  },
64
64
  "dependencies": {
65
- "@stacksjs/config": "0.70.162",
65
+ "@stacksjs/config": "0.70.164",
66
66
  "@stacksjs/bumpx": "^0.2.6",
67
67
  "@stacksjs/bunpress": "^0.1.18",
68
68
  "@stacksjs/logsmith": "^0.2.3",
69
- "@stacksjs/registry": "0.70.162",
70
- "@stacksjs/ts-cloud": "^0.7.56",
69
+ "@stacksjs/registry": "0.70.164",
70
+ "@stacksjs/ts-cloud": "^0.7.60",
71
71
  "@stacksjs/ts-md": "^0.1.1"
72
72
  },
73
73
  "devDependencies": {
74
- "@stacksjs/api": "0.70.162",
75
- "@stacksjs/cli": "0.70.162",
76
- "@stacksjs/config": "0.70.162",
77
- "@stacksjs/database": "0.70.162",
74
+ "@stacksjs/api": "0.70.164",
75
+ "@stacksjs/cli": "0.70.164",
76
+ "@stacksjs/config": "0.70.164",
77
+ "@stacksjs/database": "0.70.164",
78
78
  "@stacksjs/tlsx": "^0.13.2",
79
79
  "better-dx": "^0.2.17",
80
- "@stacksjs/dns": "0.70.162",
81
- "@stacksjs/enums": "0.70.162",
82
- "@stacksjs/env": "0.70.162",
83
- "@stacksjs/error-handling": "0.70.162",
84
- "@stacksjs/logging": "0.70.162",
85
- "@stacksjs/path": "0.70.162",
86
- "@stacksjs/security": "0.70.162",
87
- "@stacksjs/storage": "0.70.162",
88
- "@stacksjs/strings": "0.70.162",
89
- "@stacksjs/utils": "0.70.162",
90
- "@stacksjs/validation": "0.70.162"
80
+ "@stacksjs/dns": "0.70.164",
81
+ "@stacksjs/enums": "0.70.164",
82
+ "@stacksjs/env": "0.70.164",
83
+ "@stacksjs/error-handling": "0.70.164",
84
+ "@stacksjs/logging": "0.70.164",
85
+ "@stacksjs/path": "0.70.164",
86
+ "@stacksjs/security": "0.70.164",
87
+ "@stacksjs/storage": "0.70.164",
88
+ "@stacksjs/strings": "0.70.164",
89
+ "@stacksjs/utils": "0.70.164",
90
+ "@stacksjs/validation": "0.70.164"
91
91
  },
92
92
  "peerDependencies": {
93
93
  "pickier": "^0.1.35"