@stacksjs/actions 0.70.161 → 0.70.163

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/dist/blog.js CHANGED
@@ -208,7 +208,7 @@ async function blogConfig(bp, fm) {
208
208
  title: fm.title || cfg.title,
209
209
  description: fm.description || cfg.description,
210
210
  docsDir: CONTENT_DIR,
211
- theme: "vitepress",
211
+ theme: "bun",
212
212
  markdown: { ...bp.defaultConfig.markdown, css: `${baseCss}
213
213
  ${themeCss}` },
214
214
  themeConfig: {
@@ -1,10 +1,11 @@
1
- import { chmodSync, copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
1
+ import { createHash } from "node:crypto";
2
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
3
  import { basename, join } from "node:path";
3
4
  import process from "node:process";
4
5
  import { log, runCommand } from "@stacksjs/cli";
5
6
  import { corePath, projectPath, storagePath } from "@stacksjs/path";
6
- import { resolveCraftBinary } from "@stacksjs/desktop";
7
- const outputDir = storagePath("framework/desktop-dist"), launcherName = process.platform === "win32" ? "stacks-desktop.exe" : "stacks-desktop", runtimeName = process.platform === "win32" ? "craft-runtime.exe" : "craft-runtime", appUrl = process.env.DESKTOP_URL || process.env.APP_URL;
7
+ import { assertDesktopReleaseChannel, resolveCraftBinary } from "@stacksjs/desktop";
8
+ const outputDir = storagePath("framework/desktop-dist"), launcherName = process.platform === "win32" ? "stacks-desktop.exe" : "stacks-desktop", runtimeName = process.platform === "win32" ? "craft-runtime.exe" : "craft-runtime", appUrl = process.env.DESKTOP_URL || process.env.APP_URL, releaseChannel = process.env.DESKTOP_RELEASE_CHANNEL === "stable" ? "stable" : "experimental", support = assertDesktopReleaseChannel(releaseChannel);
8
9
  if (!appUrl)
9
10
  throw Error("Desktop builds require APP_URL or DESKTOP_URL so the native app knows which Stacks application to open");
10
11
  const url = new URL(/^https?:\/\//.test(appUrl) ? appUrl : `https://${appUrl}`), craftBinary = resolveCraftBinary();
@@ -29,7 +30,30 @@ writeFileSync(join(outputDir, "desktop.json"), `${JSON.stringify({
29
30
  height: 900,
30
31
  darkMode: !1,
31
32
  systemTray: !0,
32
- hideDockIcon: !1
33
+ hideDockIcon: !1,
34
+ releaseChannel,
35
+ platform: process.platform,
36
+ architecture: process.arch
33
37
  }, null, 2)}
34
38
  `);
39
+ const gitResult = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: projectPath() });
40
+ if (gitResult.exitCode !== 0)
41
+ throw Error("Desktop builds require an exact Git source revision");
42
+ const sourceRevision = gitResult.stdout.toString().trim(), artifacts = [launcherName, runtimeName, "desktop.json"].map((name) => {
43
+ const contents = readFileSync(join(outputDir, name));
44
+ return { name, bytes: contents.byteLength, sha256: createHash("sha256").update(contents).digest("hex") };
45
+ });
46
+ writeFileSync(join(outputDir, "provenance.json"), `${JSON.stringify({
47
+ schemaVersion: "1.0.0",
48
+ sourceRepository: "https://github.com/stacksjs/stacks",
49
+ sourceRevision,
50
+ builtWith: { bun: Bun.version, craftSha256: artifacts.find((artifact) => artifact.name === runtimeName)?.sha256 },
51
+ target: { platform: process.platform, architecture: process.arch, status: support.status, osVersions: support.osVersions },
52
+ releaseChannel,
53
+ artifacts
54
+ }, null, 2)}
55
+ `);
56
+ writeFileSync(join(outputDir, "checksums.sha256"), `${artifacts.map((artifact) => `${artifact.sha256} ${artifact.name}`).join(`
57
+ `)}
58
+ `);
35
59
  log.success(`Built Craft desktop application in ${outputDir}`);
package/dist/build.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { BuildOptions } from '@stacksjs/types';
2
2
  export declare function invoke(options: BuildOptions): Promise<void>;
3
3
  export declare function build(options: BuildOptions): Promise<void>;
4
4
  export declare function componentLibraries(options: BuildOptions): Promise<void>;
5
- export declare function vueComponentLibrary(options: BuildOptions): Promise<void>;
5
+ export declare function stxComponentLibrary(options: BuildOptions): Promise<void>;
6
6
  export declare function webComponentLibrary(options: BuildOptions): Promise<void>;
7
7
  export declare function docs(options: BuildOptions): Promise<void>;
8
8
  export declare function stacks(options: BuildOptions): Promise<void>;
package/dist/build.js CHANGED
@@ -21,10 +21,10 @@ export async function build(options) {
21
21
  }
22
22
  export async function componentLibraries(options) {
23
23
  await runNpmScript(NpmScript.GenerateEntries, options);
24
- await vueComponentLibrary(options);
24
+ await stxComponentLibrary(options);
25
25
  await webComponentLibrary(options);
26
26
  }
27
- export async function vueComponentLibrary(options) {
27
+ export async function stxComponentLibrary(options) {
28
28
  if (hasComponents()) {
29
29
  log.info("Building your component library...");
30
30
  await runNpmScript(NpmScript.BuildComponents, options);
package/dist/make.js CHANGED
@@ -268,10 +268,10 @@ export async function makeStack(options) {
268
268
  log.info("");
269
269
  log.info(` cd ${name}`);
270
270
  log.info(" # Add your models, actions, views, etc.");
271
- log.info(" # Then publish: bun publish");
271
+ log.info(" # Push the repository, then submit its GitHub source to the Stacks registry.");
272
272
  log.info("");
273
273
  log.info(" Users install it with:");
274
- log.info(` buddy stack:install ${name}`);
274
+ log.info(` buddy add ${shortName}`);
275
275
  } catch (error) {
276
276
  log.error("There was an error creating your stack", error);
277
277
  process.exit(ExitCode.FatalError);
package/dist/stacks.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import type { StackInstallOptions, StackListEntry, StackManifestEntry, StackUninstallOptions } from '@stacksjs/types';
2
2
  export declare function installStack(options: StackInstallOptions): Promise<StackManifestEntry | null>;
3
3
  export declare function uninstallStack(options: StackUninstallOptions): Promise<boolean>;
4
- export declare function listStacks(): Promise<StackListEntry[]>;
4
+ export declare function listStacks(project?: string): Promise<StackListEntry[]>;
package/dist/stacks.js CHANGED
@@ -1,9 +1,19 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
+ import process from "node:process";
1
5
  import { log } from "@stacksjs/logging";
2
- import { dirname, join, projectPath, relative, stacksBackupPath, stacksLockPath } from "@stacksjs/path";
6
+ import { registry } from "@stacksjs/registry";
3
7
  import { copyFile, createFolder, deleteFile, deleteEmptyFolders, doesFolderExist, fs } from "@stacksjs/storage";
4
- const STACK_DIRS = ["app", "config", "database", "resources", "routes", "public", "locales"];
5
- async function readStackLock() {
6
- const lockPath = stacksLockPath();
8
+ const STACK_DIRS = ["app", "config", "database", "resources", "routes", "public", "locales", "docs"];
9
+ function stackLockPath(projectRoot) {
10
+ return join(projectRoot, "storage", "framework", "stacks.lock.json");
11
+ }
12
+ function stackBackupPath(projectRoot, stackName) {
13
+ return join(projectRoot, "storage", "framework", "stacks", "backups", stackName);
14
+ }
15
+ async function readStackLock(projectRoot) {
16
+ const lockPath = stackLockPath(projectRoot);
7
17
  try {
8
18
  if (fs.existsSync(lockPath)) {
9
19
  const content = await Bun.file(lockPath).text();
@@ -16,35 +26,47 @@ async function readStackLock() {
16
26
  stacks: {}
17
27
  };
18
28
  }
19
- async function writeStackLock(lock) {
20
- const lockPath = stacksLockPath(), lockDir = dirname(lockPath);
29
+ async function writeStackLock(projectRoot, lock) {
30
+ const lockPath = stackLockPath(projectRoot), lockDir = dirname(lockPath);
21
31
  if (!fs.existsSync(lockDir))
22
32
  await createFolder(lockDir);
23
33
  lock.generatedAt = new Date().toISOString();
24
34
  await Bun.write(lockPath, JSON.stringify(lock, null, 2));
25
35
  }
26
- function resolveStackPackagePath(name) {
27
- const pantryDir = projectPath("pantry"), directPath = join(pantryDir, name);
28
- if (fs.existsSync(join(directPath, "package.json")))
29
- return directPath;
30
- const scopedPath = join(pantryDir, "@stacksjs", name);
31
- if (fs.existsSync(join(scopedPath, "package.json")))
32
- return scopedPath;
33
- if (name.startsWith("@")) {
34
- const fullPath = join(pantryDir, name);
35
- if (fs.existsSync(join(fullPath, "package.json")))
36
- return fullPath;
37
- }
38
- const patterns = ["*/package.json", "@*/*/package.json"];
39
- for (const pattern of patterns) {
40
- const glob = new Bun.Glob(pattern);
41
- for (const file of glob.scanSync({ cwd: pantryDir, absolute: !0, onlyFiles: !0 }))
42
- try {
43
- if (JSON.parse(fs.readFileSync(file, "utf-8"))?.stacks?.name === name)
44
- return dirname(file);
45
- } catch {}
36
+ function resolveProjectRoot(project) {
37
+ return resolve(project || process.cwd());
38
+ }
39
+ function resolveRegistryEntry(name) {
40
+ const entry = registry.find((candidate) => {
41
+ if (typeof candidate === "string")
42
+ return candidate === name;
43
+ return candidate.name === name || candidate.package === name;
44
+ });
45
+ if (!entry || typeof entry === "string")
46
+ return null;
47
+ if (!entry.github)
48
+ return null;
49
+ return { name: entry.name, github: entry.github, package: entry.package };
50
+ }
51
+ async function checkoutRegistryStack(github) {
52
+ const tempRoot = mkdtempSync(join(tmpdir(), "stacks-stack-")), checkoutPath = join(tempRoot, "source"), source = `https://github.com/${github}.git`;
53
+ if (await Bun.spawn(["git", "clone", "--depth", "1", "--quiet", source, checkoutPath], {
54
+ stdout: "inherit",
55
+ stderr: "inherit"
56
+ }).exited !== 0) {
57
+ rmSync(tempRoot, { recursive: !0, force: !0 });
58
+ throw Error(`Could not pull ${source}`);
46
59
  }
47
- return null;
60
+ return {
61
+ path: checkoutPath,
62
+ cleanup: () => rmSync(tempRoot, { recursive: !0, force: !0 })
63
+ };
64
+ }
65
+ function isSafeStackDirectory(directory) {
66
+ return directory.length > 0 && !isAbsolute(directory) && !directory.includes("..") && !directory.includes("\\");
67
+ }
68
+ function fileChecksum(path) {
69
+ return new Bun.CryptoHasher("sha256").update(fs.readFileSync(path)).digest("hex");
48
70
  }
49
71
  function collectFiles(dir, baseDir) {
50
72
  const files = [];
@@ -55,118 +77,151 @@ function collectFiles(dir, baseDir) {
55
77
  const fullPath = join(dir, entry.name);
56
78
  if (entry.isDirectory())
57
79
  files.push(...collectFiles(fullPath, baseDir));
58
- else
59
- files.push(relative(baseDir, fullPath));
80
+ else if (entry.isSymbolicLink())
81
+ throw Error(`Stack contains an unsupported symbolic link: ${relative(baseDir, fullPath)}`);
82
+ else {
83
+ const relPath = relative(baseDir, fullPath);
84
+ if (!isSafeStackDirectory(relPath))
85
+ throw Error(`Stack contains an unsafe path: ${relPath}`);
86
+ files.push(relPath);
87
+ }
60
88
  }
61
89
  return files;
62
90
  }
63
91
  export async function installStack(options) {
64
- const { name, force = !1, dryRun = !1, verbose = !1 } = options, conflict = force ? "overwrite" : options.conflict ?? "skip";
92
+ const { name, force = !1, dryRun = !1, verbose = !1 } = options, conflict = force ? "overwrite" : options.conflict ?? "skip", projectRoot = resolveProjectRoot(options.project);
65
93
  log.info(`Installing stack "${name}"...`);
66
- const stackPath = resolveStackPackagePath(name);
67
- if (!stackPath) {
68
- log.error(`Stack "${name}" not found in pantry. Install it first with: bun add <package-name>`);
94
+ if (!fs.existsSync(join(projectRoot, "package.json"))) {
95
+ log.error(`No Stacks project found at "${projectRoot}"`);
96
+ return null;
97
+ }
98
+ const registryEntry = resolveRegistryEntry(name);
99
+ if (!registryEntry) {
100
+ log.error(`Stack "${name}" is not in the Stacks registry. Run "buddy stack:list" to see available stacks.`);
69
101
  return null;
70
102
  }
71
- let pkg, meta;
103
+ let checkout;
72
104
  try {
73
- const pkgContent = await Bun.file(join(stackPath, "package.json")).text();
74
- pkg = JSON.parse(pkgContent);
75
- meta = pkg.stacks;
76
- if (!meta?.name) {
77
- log.error(`Package at "${stackPath}" does not have a valid "stacks" field in package.json`);
78
- return null;
79
- }
105
+ checkout = await checkoutRegistryStack(registryEntry.github);
80
106
  } catch (error) {
81
- log.error(`Failed to read stack package.json: ${error}`);
107
+ log.error(error instanceof Error ? error.message : String(error));
82
108
  return null;
83
109
  }
84
- const lock = await readStackLock(), packageName = pkg.name || name;
85
- if (lock.stacks[packageName] && !force) {
86
- log.warn(`Stack "${meta.name}" is already installed. Use --force to reinstall.`);
87
- return lock.stacks[packageName];
88
- }
89
- const dirsToScan = meta.directories ?? STACK_DIRS, allFiles = [];
90
- for (const dir of dirsToScan) {
91
- const dirPath = join(stackPath, dir);
92
- if (fs.existsSync(dirPath)) {
93
- const files = collectFiles(dirPath, stackPath);
94
- allFiles.push(...files);
110
+ try {
111
+ const stackPath = checkout.path;
112
+ let pkg, meta;
113
+ try {
114
+ const pkgContent = await Bun.file(join(stackPath, "package.json")).text();
115
+ pkg = JSON.parse(pkgContent);
116
+ meta = pkg.stacks;
117
+ if (!meta?.name || meta.name !== registryEntry.name) {
118
+ log.error(`Registry source "${registryEntry.github}" does not declare stacks.name as "${registryEntry.name}"`);
119
+ return null;
120
+ }
121
+ } catch (error) {
122
+ log.error(`Failed to read stack metadata: ${error}`);
123
+ return null;
95
124
  }
96
- }
97
- if (allFiles.length === 0) {
98
- log.warn(`Stack "${meta.name}" has no files to install`);
99
- return null;
100
- }
101
- if (verbose)
102
- log.info(`Found ${allFiles.length} file(s) to install`);
103
- const copiedFiles = [], skippedFiles = [];
104
- for (const relPath of allFiles) {
105
- const src = join(stackPath, relPath), dest = projectPath(relPath);
106
- if (dryRun) {
107
- if (fs.existsSync(dest)) {
108
- log.info(`[dry-run] Would ${conflict}: ${relPath}`);
109
- if (conflict === "skip")
110
- skippedFiles.push(relPath);
111
- else
112
- copiedFiles.push(relPath);
113
- } else {
114
- log.info(`[dry-run] Would copy: ${relPath}`);
115
- copiedFiles.push(relPath);
125
+ const lock = await readStackLock(projectRoot), packageName = pkg.name || registryEntry.package || name;
126
+ if (lock.stacks[packageName] && !force) {
127
+ log.warn(`Stack "${meta.name}" is already installed. Use --force to reinstall.`);
128
+ return lock.stacks[packageName];
129
+ }
130
+ const dirsToScan = meta.directories ?? STACK_DIRS;
131
+ if (dirsToScan.some((directory) => !isSafeStackDirectory(directory))) {
132
+ log.error(`Stack "${meta.name}" declares an unsafe project directory`);
133
+ return null;
134
+ }
135
+ const allFiles = [];
136
+ try {
137
+ for (const dir of dirsToScan) {
138
+ const dirPath = join(stackPath, dir);
139
+ if (fs.existsSync(dirPath))
140
+ allFiles.push(...collectFiles(dirPath, stackPath));
116
141
  }
117
- continue;
142
+ } catch (error) {
143
+ log.error(error instanceof Error ? error.message : String(error));
144
+ return null;
145
+ }
146
+ if (allFiles.length === 0) {
147
+ log.warn(`Stack "${meta.name}" has no project files to install`);
148
+ return null;
118
149
  }
119
- if (fs.existsSync(dest))
120
- switch (conflict) {
121
- case "skip":
122
- if (verbose)
123
- log.warn(`Skipping (file exists): ${relPath}`);
124
- skippedFiles.push(relPath);
125
- continue;
126
- case "backup": {
127
- const backupDir = stacksBackupPath(meta.name), backupDest = join(backupDir, relPath), backupDestDir = dirname(backupDest);
128
- if (!fs.existsSync(backupDestDir))
129
- await createFolder(backupDestDir);
130
- copyFile(dest, backupDest);
131
- if (verbose)
132
- log.info(`Backed up: ${relPath}`);
133
- break;
150
+ if (verbose)
151
+ log.info(`Pulled ${registryEntry.github} and found ${allFiles.length} project file(s)`);
152
+ const copiedFiles = [], skippedFiles = [], checksums = {};
153
+ for (const relPath of allFiles) {
154
+ const src = join(stackPath, relPath), dest = join(projectRoot, relPath);
155
+ if (dryRun) {
156
+ if (fs.existsSync(dest)) {
157
+ log.info(`[dry-run] Would ${conflict}: ${relPath}`);
158
+ if (conflict === "skip")
159
+ skippedFiles.push(relPath);
160
+ else
161
+ copiedFiles.push(relPath);
162
+ } else {
163
+ log.info(`[dry-run] Would copy: ${relPath}`);
164
+ copiedFiles.push(relPath);
134
165
  }
135
- case "overwrite":
136
- if (verbose)
137
- log.warn(`Overwriting: ${relPath}`);
138
- break;
166
+ continue;
139
167
  }
140
- const destDir = dirname(dest);
141
- if (!fs.existsSync(destDir))
142
- await createFolder(destDir);
143
- copyFile(src, dest);
144
- copiedFiles.push(relPath);
145
- if (verbose)
146
- log.info(`Copied: ${relPath}`);
147
- }
148
- if (dryRun) {
149
- log.info(`[dry-run] Would install ${copiedFiles.length} file(s), skip ${skippedFiles.length} file(s)`);
150
- return null;
168
+ if (fs.existsSync(dest))
169
+ switch (conflict) {
170
+ case "skip":
171
+ if (verbose)
172
+ log.warn(`Skipping (file exists): ${relPath}`);
173
+ skippedFiles.push(relPath);
174
+ continue;
175
+ case "backup": {
176
+ const backupDir = stackBackupPath(projectRoot, meta.name), backupDest = join(backupDir, relPath), backupDestDir = dirname(backupDest);
177
+ if (!fs.existsSync(backupDestDir))
178
+ await createFolder(backupDestDir);
179
+ copyFile(dest, backupDest);
180
+ if (verbose)
181
+ log.info(`Backed up: ${relPath}`);
182
+ break;
183
+ }
184
+ case "overwrite":
185
+ if (verbose)
186
+ log.warn(`Overwriting: ${relPath}`);
187
+ break;
188
+ }
189
+ const destDir = dirname(dest);
190
+ if (!fs.existsSync(destDir))
191
+ await createFolder(destDir);
192
+ copyFile(src, dest);
193
+ copiedFiles.push(relPath);
194
+ checksums[relPath] = fileChecksum(dest);
195
+ if (verbose)
196
+ log.info(`Copied: ${relPath}`);
197
+ }
198
+ const entry = {
199
+ packageName,
200
+ name: meta.name,
201
+ version: pkg.version || "0.0.0",
202
+ installedAt: new Date().toISOString(),
203
+ files: copiedFiles,
204
+ checksums,
205
+ source: registryEntry.github,
206
+ skipped: skippedFiles,
207
+ conflictStrategy: conflict
208
+ };
209
+ if (dryRun) {
210
+ log.info(`[dry-run] Would install ${copiedFiles.length} file(s), skip ${skippedFiles.length} file(s)`);
211
+ return entry;
212
+ }
213
+ lock.stacks[packageName] = entry;
214
+ await writeStackLock(projectRoot, lock);
215
+ log.success(`Installed stack "${meta.name}" from ${registryEntry.github} (${copiedFiles.length} file(s) copied, ${skippedFiles.length} skipped)`);
216
+ return entry;
217
+ } finally {
218
+ checkout.cleanup();
151
219
  }
152
- const entry = {
153
- packageName,
154
- name: meta.name,
155
- version: pkg.version || "0.0.0",
156
- installedAt: new Date().toISOString(),
157
- files: copiedFiles,
158
- skipped: skippedFiles,
159
- conflictStrategy: conflict
160
- };
161
- lock.stacks[packageName] = entry;
162
- await writeStackLock(lock);
163
- log.success(`Installed stack "${meta.name}" (${copiedFiles.length} file(s) copied, ${skippedFiles.length} skipped)`);
164
- return entry;
165
220
  }
166
221
  export async function uninstallStack(options) {
167
- const { name, force = !1, verbose = !1 } = options;
222
+ const { name, force = !1, verbose = !1 } = options, projectRoot = resolveProjectRoot(options.project);
168
223
  log.info(`Uninstalling stack "${name}"...`);
169
- const lock = await readStackLock();
224
+ const lock = await readStackLock(projectRoot);
170
225
  let entryKey = null, entry = null;
171
226
  for (const [key, value] of Object.entries(lock.stacks))
172
227
  if (value.name === name || key === name) {
@@ -179,24 +234,20 @@ export async function uninstallStack(options) {
179
234
  return !1;
180
235
  }
181
236
  let removedCount = 0;
237
+ const remainingFiles = [];
182
238
  for (const relPath of entry.files) {
183
- const filePath = projectPath(relPath);
239
+ const filePath = join(projectRoot, relPath);
184
240
  if (!fs.existsSync(filePath)) {
185
241
  if (verbose)
186
242
  log.info(`Already removed: ${relPath}`);
187
243
  continue;
188
244
  }
189
245
  if (!force) {
190
- const stackPath = resolveStackPackagePath(name);
191
- if (stackPath) {
192
- const srcPath = join(stackPath, relPath);
193
- if (fs.existsSync(srcPath)) {
194
- const srcContent = await Bun.file(srcPath).text(), destContent = await Bun.file(filePath).text();
195
- if (srcContent !== destContent) {
196
- log.warn(`Skipping modified file: ${relPath} (use --force to remove)`);
197
- continue;
198
- }
199
- }
246
+ const installedChecksum = entry.checksums?.[relPath];
247
+ if (!installedChecksum || fileChecksum(filePath) !== installedChecksum) {
248
+ log.warn(`Skipping modified file: ${relPath} (use --force to remove)`);
249
+ remainingFiles.push(relPath);
250
+ continue;
200
251
  }
201
252
  }
202
253
  await deleteFile(filePath);
@@ -204,11 +255,19 @@ export async function uninstallStack(options) {
204
255
  if (verbose)
205
256
  log.info(`Removed: ${relPath}`);
206
257
  }
207
- const backupDir = stacksBackupPath(entry.name);
258
+ if (remainingFiles.length > 0) {
259
+ entry.files = remainingFiles;
260
+ entry.checksums = Object.fromEntries(remainingFiles.map((file) => [file, entry.checksums?.[file]]).filter((item) => typeof item[1] === "string"));
261
+ lock.stacks[entryKey] = entry;
262
+ await writeStackLock(projectRoot, lock);
263
+ log.warn(`Stack "${entry.name}" is still tracked because ${remainingFiles.length} modified file(s) remain`);
264
+ return !1;
265
+ }
266
+ const backupDir = stackBackupPath(projectRoot, entry.name);
208
267
  if (fs.existsSync(backupDir) && doesFolderExist(backupDir)) {
209
268
  const backupFiles = collectFiles(backupDir, backupDir);
210
269
  for (const relPath of backupFiles) {
211
- const backupSrc = join(backupDir, relPath), dest = projectPath(relPath), destDir = dirname(dest);
270
+ const backupSrc = join(backupDir, relPath), dest = join(projectRoot, relPath), destDir = dirname(dest);
212
271
  if (!fs.existsSync(destDir))
213
272
  await createFolder(destDir);
214
273
  copyFile(backupSrc, dest);
@@ -218,38 +277,30 @@ export async function uninstallStack(options) {
218
277
  fs.rmSync(backupDir, { recursive: !0, force: !0 });
219
278
  }
220
279
  for (const dir of STACK_DIRS) {
221
- const dirPath = projectPath(dir);
280
+ const dirPath = join(projectRoot, dir);
222
281
  if (fs.existsSync(dirPath))
223
282
  await deleteEmptyFolders(dirPath);
224
283
  }
225
284
  delete lock.stacks[entryKey];
226
- await writeStackLock(lock);
285
+ await writeStackLock(projectRoot, lock);
227
286
  log.success(`Uninstalled stack "${entry.name}" (${removedCount} file(s) removed)`);
228
287
  return !0;
229
288
  }
230
- export async function listStacks() {
231
- const entries = [], lock = await readStackLock(), pantryDir = projectPath("pantry");
232
- if (fs.existsSync(pantryDir)) {
233
- const patterns = ["*/package.json", "@*/*/package.json"];
234
- for (const pattern of patterns) {
235
- const glob = new Bun.Glob(pattern);
236
- for (const file of glob.scanSync({ cwd: pantryDir, absolute: !0, onlyFiles: !0 }))
237
- try {
238
- const pkgContent = fs.readFileSync(file, "utf-8"), pkg = JSON.parse(pkgContent);
239
- if (!pkg?.stacks?.name)
240
- continue;
241
- const packageName = pkg.name, installed = !!lock.stacks[packageName], lockEntry = lock.stacks[packageName];
242
- entries.push({
243
- name: pkg.stacks.name,
244
- packageName,
245
- version: pkg.version || "0.0.0",
246
- description: pkg.stacks.description,
247
- installed,
248
- installedAt: lockEntry?.installedAt,
249
- fileCount: lockEntry?.files.length
250
- });
251
- } catch {}
252
- }
289
+ export async function listStacks(project) {
290
+ const entries = [], projectRoot = resolveProjectRoot(project), lock = await readStackLock(projectRoot);
291
+ for (const candidate of registry) {
292
+ if (typeof candidate === "string")
293
+ continue;
294
+ const packageName = candidate.package || candidate.name, lockEntry = lock.stacks[packageName];
295
+ entries.push({
296
+ name: candidate.name,
297
+ packageName,
298
+ version: lockEntry?.version || "latest",
299
+ description: candidate.description,
300
+ installed: Boolean(lockEntry),
301
+ installedAt: lockEntry?.installedAt,
302
+ fileCount: lockEntry?.files.length
303
+ });
253
304
  }
254
305
  for (const [packageName, entry] of Object.entries(lock.stacks))
255
306
  if (!entries.some((e) => e.packageName === packageName))
package/dist/templates.js CHANGED
@@ -62,22 +62,25 @@ export default new Action({
62
62
  return { ok: true, userId: user.id, data }
63
63
  },
64
64
  })`,
65
- component: `<script setup lang="ts">
66
- console.log('Hello World component created')
65
+ component: `<script server>
66
+ interface ComponentProps {
67
+ message?: string
68
+ }
69
+
70
+ const { message = 'Hello World component created' } = defineProps<ComponentProps>()
67
71
  </script>
68
72
 
69
73
  <template>
70
- <div>
71
- Some HTML block
72
- </div>
74
+ <div>{{ message }}</div>
73
75
  </template>`,
74
- page: `<script setup lang="ts">
75
- console.log('Hello World page created')
76
+ page: `<script server>
77
+ const heading = 'Hello World page created'
76
78
  </script>
77
79
 
78
80
  <template>
79
81
  <div>
80
- Visit http://127.0.0.1/{0}
82
+ <h1>{{ heading }}</h1>
83
+ <p>Visit /{0}</p>
81
84
  </div>
82
85
  </template>`,
83
86
  function: `// reactive state
@@ -1,11 +1,2 @@
1
- import process from "node:process";
2
- import { log } from "@stacksjs/cli";
3
- import { projectPath } from "@stacksjs/path";
4
- const proc = Bun.spawn(["sh", "-c", "bun test ./tests/feature/**"], {
5
- cwd: projectPath(),
6
- stdio: ["inherit", "inherit", "inherit"]
7
- }), exitCode = await proc.exited;
8
- if (exitCode !== 0) {
9
- log.error("Tests failed");
10
- process.exit(exitCode ?? 1);
11
- }
1
+ import { runTestSuites } from "./runner";
2
+ await runTestSuites(["feature"]);
@@ -1,11 +1,2 @@
1
- import process from "node:process";
2
- import { log } from "@stacksjs/cli";
3
- import { projectPath } from "@stacksjs/path";
4
- const proc = Bun.spawn(["sh", "-c", "bun test --timeout 30000 ./tests/feature/** ./tests/integration/** ./tests/unit/**"], {
5
- cwd: projectPath(),
6
- stdio: ["inherit", "inherit", "inherit"]
7
- }), exitCode = await proc.exited;
8
- if (exitCode !== 0) {
9
- log.error("Tests failed");
10
- process.exit(exitCode ?? 1);
11
- }
1
+ import { runTestSuites } from "./runner";
2
+ await runTestSuites([""], 30000);
@@ -0,0 +1 @@
1
+ export declare function runTestSuites(suites: string[], timeout?: number): Promise<void>;
@@ -0,0 +1,30 @@
1
+ import process from "node:process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { log } from "@stacksjs/cli";
5
+ import { projectPath } from "@stacksjs/path";
6
+ const testFiles = new Bun.Glob("**/*.{test,spec}.{js,jsx,ts,tsx,mjs,mts,cjs,cts}");
7
+ export async function runTestSuites(suites, timeout) {
8
+ const project = projectPath(), filters = suites.flatMap((suite) => {
9
+ const suiteDirectory = join(project, "tests", suite);
10
+ if (!existsSync(suiteDirectory))
11
+ return [];
12
+ return [...testFiles.scanSync({ cwd: suiteDirectory, onlyFiles: !0 })].map((file) => suite ? `./tests/${suite}/${file}` : `./tests/${file}`);
13
+ });
14
+ if (filters.length === 0) {
15
+ log.info("No matching tests found.");
16
+ return;
17
+ }
18
+ const args = ["bun", "test"];
19
+ if (timeout)
20
+ args.push("--timeout", String(timeout));
21
+ args.push(...filters);
22
+ const exitCode = await Bun.spawn(args, {
23
+ cwd: project,
24
+ stdio: ["inherit", "inherit", "inherit"]
25
+ }).exited;
26
+ if (exitCode !== 0) {
27
+ log.error("Tests failed");
28
+ process.exit(exitCode ?? 1);
29
+ }
30
+ }
package/dist/test/unit.js CHANGED
@@ -1,11 +1,2 @@
1
- import process from "node:process";
2
- import { log } from "@stacksjs/cli";
3
- import { projectPath } from "@stacksjs/path";
4
- const proc = Bun.spawn(["sh", "-c", "bun test ./tests/unit/**"], {
5
- cwd: projectPath(),
6
- stdio: ["inherit", "inherit", "inherit"]
7
- }), exitCode = await proc.exited;
8
- if (exitCode !== 0) {
9
- log.error("Tests failed");
10
- process.exit(exitCode ?? 1);
11
- }
1
+ import { runTestSuites } from "./runner";
2
+ await runTestSuites(["unit"]);
@@ -1,5 +1,16 @@
1
1
  export declare function hasStacksDependency(pkg: PkgJson): boolean;
2
2
  export declare function standalonePackageUpdateCommand(): string;
3
+ /** Strip a range operator (`^`, `~`, `>=`, …) so a declared range compares to a bare version. */
4
+ export declare function baseVersion(range: string): string;
5
+ /*` tooling
6
+ * (e.g. `better-dx`) are excluded, so they are never force-bumped to a framework
7
+ * version they don't publish (stacksjs/stacks#2078).
8
+ */
9
+ export declare function lockstepPackages(metaDeps: Record<string, string>, target: string): Set<string>;
10
+ /*` packages — is left
11
+ * untouched. Returns the set of applied changes.
12
+ */
13
+ export declare function applyBumps(pkg: PkgJson, target: string, lockstep: Set<string>): Array<{ name: string, from: string, to: string }>;
3
14
  /**
4
15
  * Upgrade a node_modules app to a published framework version. Called by the
5
16
  * framework upgrade script when no vendored `storage/framework/core` exists.
@@ -46,17 +46,27 @@ async function resolveTarget(options) {
46
46
  throw Error(`The \`${tag}\` dist-tag is not published for \`stacks\`.`);
47
47
  version = resolved;
48
48
  }
49
- const coreDeps = new Set(Object.keys(versions[version]?.dependencies ?? {}));
50
- return { version, coreDeps };
49
+ const metaDeps = versions[version]?.dependencies ?? {};
50
+ return { version, metaDeps };
51
51
  }
52
- function applyBumps(pkg, target, coreDeps) {
53
- const changes = [], isLockstep = (name) => name === "stacks" || name.startsWith("@stacksjs/") && coreDeps.has(name);
52
+ export function baseVersion(range) {
53
+ return range.replace(/^[\^~>=<\s]+/, "").trim();
54
+ }
55
+ export function lockstepPackages(metaDeps, target) {
56
+ const lockstep = new Set(["stacks"]);
57
+ for (const [name, range] of Object.entries(metaDeps))
58
+ if (name.startsWith("@stacksjs/") && baseVersion(range) === target)
59
+ lockstep.add(name);
60
+ return lockstep;
61
+ }
62
+ export function applyBumps(pkg, target, lockstep) {
63
+ const changes = [];
54
64
  for (const field of ["dependencies", "devDependencies"]) {
55
65
  const deps = pkg[field];
56
66
  if (!deps)
57
67
  continue;
58
68
  for (const [name, spec] of Object.entries(deps)) {
59
- if (!isLockstep(name))
69
+ if (!lockstep.has(name))
60
70
  continue;
61
71
  const next = `${/^[\^~]/.test(spec) ? spec[0] : /^\d/.test(spec) ? "" : "^"}${target}`;
62
72
  if (spec !== next) {
@@ -76,7 +86,7 @@ export async function upgradeStacksPackages(projectRoot, options) {
76
86
  const raw = readFileSync(pkgPath, "utf-8"), pkg = JSON.parse(raw), current = pkg.dependencies?.stacks ?? pkg.devDependencies?.stacks;
77
87
  if (!hasStacksDependency(pkg))
78
88
  return updateStandalonePackage(projectRoot, options);
79
- const { version: target, coreDeps } = await resolveTarget(options).catch((err) => {
89
+ const { version: target, metaDeps } = await resolveTarget(options).catch((err) => {
80
90
  console.error(`\u2717 ${err.message}`);
81
91
  process.exit(1);
82
92
  });
@@ -85,7 +95,7 @@ export async function upgradeStacksPackages(projectRoot, options) {
85
95
  if (current)
86
96
  console.log(` current \`stacks\` constraint: ${current}
87
97
  `);
88
- const changes = applyBumps(pkg, target, coreDeps);
98
+ const lockstep = lockstepPackages(metaDeps, target), changes = applyBumps(pkg, target, lockstep);
89
99
  if (changes.length === 0 && !options.force) {
90
100
  console.log(`\u2714 Already up to date \u2014 every framework dependency matches the target.
91
101
  `);
@@ -110,7 +120,7 @@ export async function upgradeStacksPackages(projectRoot, options) {
110
120
  writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}${trailing}`);
111
121
  console.log("\u2714 Updated package.json");
112
122
  }
113
- const frameworkPkgs = ["stacks", ...[...coreDeps].filter((n) => n.startsWith("@stacksjs/"))];
123
+ const frameworkPkgs = [...lockstep];
114
124
  if (options.noPostinstall) {
115
125
  console.log(" --no-postinstall: skipping install. Run this to pull the new versions:");
116
126
  console.log(` bun update ${frameworkPkgs.slice(0, 3).join(" ")} \u2026
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.161",
5
+ "version": "0.70.163",
6
6
  "description": "The Stacks actions.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -62,31 +62,32 @@
62
62
  "prepublishOnly": "bun run build"
63
63
  },
64
64
  "dependencies": {
65
- "@stacksjs/config": "0.70.161",
65
+ "@stacksjs/config": "0.70.163",
66
66
  "@stacksjs/bumpx": "^0.2.6",
67
67
  "@stacksjs/bunpress": "^0.1.18",
68
68
  "@stacksjs/logsmith": "^0.2.3",
69
- "@stacksjs/ts-cloud": "^0.7.49",
69
+ "@stacksjs/registry": "0.70.163",
70
+ "@stacksjs/ts-cloud": "^0.7.56",
70
71
  "@stacksjs/ts-md": "^0.1.1"
71
72
  },
72
73
  "devDependencies": {
73
- "@stacksjs/api": "0.70.161",
74
- "@stacksjs/cli": "0.70.161",
75
- "@stacksjs/config": "0.70.161",
76
- "@stacksjs/database": "0.70.161",
74
+ "@stacksjs/api": "0.70.163",
75
+ "@stacksjs/cli": "0.70.163",
76
+ "@stacksjs/config": "0.70.163",
77
+ "@stacksjs/database": "0.70.163",
77
78
  "@stacksjs/tlsx": "^0.13.2",
78
79
  "better-dx": "^0.2.17",
79
- "@stacksjs/dns": "0.70.161",
80
- "@stacksjs/enums": "0.70.161",
81
- "@stacksjs/env": "0.70.161",
82
- "@stacksjs/error-handling": "0.70.161",
83
- "@stacksjs/logging": "0.70.161",
84
- "@stacksjs/path": "0.70.161",
85
- "@stacksjs/security": "0.70.161",
86
- "@stacksjs/storage": "0.70.161",
87
- "@stacksjs/strings": "0.70.161",
88
- "@stacksjs/utils": "0.70.161",
89
- "@stacksjs/validation": "0.70.161"
80
+ "@stacksjs/dns": "0.70.163",
81
+ "@stacksjs/enums": "0.70.163",
82
+ "@stacksjs/env": "0.70.163",
83
+ "@stacksjs/error-handling": "0.70.163",
84
+ "@stacksjs/logging": "0.70.163",
85
+ "@stacksjs/path": "0.70.163",
86
+ "@stacksjs/security": "0.70.163",
87
+ "@stacksjs/storage": "0.70.163",
88
+ "@stacksjs/strings": "0.70.163",
89
+ "@stacksjs/utils": "0.70.163",
90
+ "@stacksjs/validation": "0.70.163"
90
91
  },
91
92
  "peerDependencies": {
92
93
  "pickier": "^0.1.35"