@peachlife/artisan 0.1.3 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peachlife/artisan",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "artisan": "./bin/artisan.mjs"
@@ -7,14 +7,30 @@ export {
7
7
  expect,
8
8
  } from "vitest";
9
9
 
10
- import { test as vitestTest } from "vitest";
10
+ import { onTestFailed as vitestOnTestFailed, test as vitestTest } from "vitest";
11
11
  import { DEFAULT_XDG_CONFIG_HOME } from "../core/constants.mjs";
12
12
  import { ContainerManager } from "../core/container-manager.mjs";
13
13
 
14
- export function createTestContext(container) {
14
+ export function createTestContext(
15
+ container,
16
+ { onTestFailed = vitestOnTestFailed } = {},
17
+ ) {
18
+ let lastRun;
19
+
20
+ onTestFailed(() => {
21
+ if (!lastRun) return;
22
+ console.error("\n--- artisan: last run() output ---");
23
+ if (lastRun.stdout) console.error("stdout:", lastRun.stdout);
24
+ if (lastRun.stderr) console.error("stderr:", lastRun.stderr);
25
+ console.error("exitCode:", lastRun.exitCode);
26
+ console.error("---");
27
+ });
28
+
15
29
  return {
16
30
  async run(arguments_ = "", options = {}) {
17
- return container.exec(arguments_, options);
31
+ const result = await container.exec(arguments_, options);
32
+ lastRun = result;
33
+ return result;
18
34
  },
19
35
  async copyFixture(localPath, containerPath) {
20
36
  return container.copyFile(localPath, containerPath);
@@ -29,6 +45,11 @@ export function createTestContext(container) {
29
45
  }
30
46
  }
31
47
  },
48
+ async shell(command, options = {}) {
49
+ const result = await container.shell(command, options);
50
+ lastRun = result;
51
+ return result;
52
+ },
32
53
  };
33
54
  }
34
55
 
@@ -81,6 +102,9 @@ function buildExtendedTest() {
81
102
  )
82
103
  .extend("setup", async ({ artisanContext }) =>
83
104
  artisanContext.setup.bind(artisanContext),
105
+ )
106
+ .extend("shell", async ({ artisanContext }) =>
107
+ artisanContext.shell.bind(artisanContext),
84
108
  );
85
109
  }
86
110
 
@@ -77,13 +77,13 @@ function buildVitestArguments({ options, merged, testFiles }) {
77
77
  value:
78
78
  options.retries === undefined ? undefined : String(options.retries),
79
79
  },
80
- { flag: "--verbose", value: options.verbose, boolean: true },
81
80
  ];
82
81
  for (const { flag, value, boolean = false } of flagRules) {
83
82
  if (!value) continue;
84
83
  vitestArguments.push(flag);
85
84
  if (!boolean) vitestArguments.push(value);
86
85
  }
86
+ if (options.verbose) vitestArguments.push("--reporter", "verbose");
87
87
  return vitestArguments;
88
88
  }
89
89
 
@@ -1,5 +1,5 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { isAbsolute, join } from "node:path";
3
3
  import { UsageError } from "../utils/errors.mjs";
4
4
  import { DEFAULT_DISTROS } from "./constants.mjs";
5
5
 
@@ -63,6 +63,14 @@ function validateConfigShape(config) {
63
63
  if (config.artifact !== undefined && typeof config.artifact !== "string") {
64
64
  throw new UsageError('"artifact" must be a string');
65
65
  }
66
+ if (
67
+ typeof config.artifact === "string" &&
68
+ (isAbsolute(config.artifact) || config.artifact.startsWith("~"))
69
+ ) {
70
+ throw new UsageError(
71
+ '"artifact" must be a repo-relative path (e.g. "./bin/mycli"), not an absolute or home-relative path',
72
+ );
73
+ }
66
74
  if (config.distros !== undefined)
67
75
  assertStringArray(config.distros, "distros");
68
76
  if (
@@ -118,8 +126,28 @@ export async function readJsonConfig(configPath, { optional = false } = {}) {
118
126
  }
119
127
  }
120
128
 
129
+ async function fileExists(filePath) {
130
+ try {
131
+ await access(filePath);
132
+ return true;
133
+ } catch (error) {
134
+ if (error?.code === "ENOENT") return false;
135
+ throw error;
136
+ }
137
+ }
138
+
121
139
  export async function loadConfig(cwd = process.cwd()) {
122
140
  const configPath = join(cwd, "artisan.config.json");
141
+ const packageJsonPath = join(cwd, "package.json");
142
+ const [configExists, packageExists] = await Promise.all([
143
+ fileExists(configPath),
144
+ fileExists(packageJsonPath),
145
+ ]);
146
+ if (!configExists && !packageExists) {
147
+ throw new UsageError(
148
+ "No artisan.config.json or package.json found — run from the project root",
149
+ );
150
+ }
123
151
  const fileConfig = await readJsonConfig(configPath, { optional: true });
124
152
  if (fileConfig.configs !== undefined) {
125
153
  validateConfigs(fileConfig.configs);
@@ -8,6 +8,8 @@ import { BINARY_MOUNT_DIR } from "./constants.mjs";
8
8
  const NO_CAP = 0;
9
9
  const SH_C = ["sh", "-c"];
10
10
  const ERR_NOT_STARTED = "Container not started";
11
+ // eslint-disable-next-line sonarjs/publicly-writable-directories
12
+ const DEFAULT_WORKDIR = "/tmp/work";
11
13
 
12
14
  function parseChar(state, char) {
13
15
  if (state.isEscaped) {
@@ -173,6 +175,17 @@ export class ContainerManager {
173
175
 
174
176
  this.#container = await builder.start();
175
177
 
178
+ const workdirResult = await this.#execBounded(
179
+ [...SH_C, `mkdir -p ${DEFAULT_WORKDIR}`],
180
+ {},
181
+ NO_CAP,
182
+ );
183
+ if (workdirResult.exitCode !== 0) {
184
+ throw new DockerError(
185
+ `failed to initialize default workdir ${DEFAULT_WORKDIR}\n${workdirResult.stderr || workdirResult.stdout}`,
186
+ );
187
+ }
188
+
176
189
  for (const command of this.#setupCommands) {
177
190
  const result = await this.#execBounded([...SH_C, command], {}, NO_CAP);
178
191
  if (result.exitCode !== 0) {
@@ -201,7 +214,7 @@ export class ContainerManager {
201
214
  const {
202
215
  env: environment = {},
203
216
  timeout = NO_CAP,
204
- cwd = BINARY_MOUNT_DIR,
217
+ cwd = DEFAULT_WORKDIR,
205
218
  } = options;
206
219
  const command = [
207
220
  this.#mountTarget,