@jordanalec/dtk 1.0.4 → 1.0.6
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 +1 -0
- package/dist/init.js +1 -1
- package/package.json +1 -1
- package/templates/init/GUIDE.md +7 -1
- package/templates/init/README.md +1 -1
- package/templates/init/{jest.config.ts → jest.config.cjs} +3 -5
- package/templates/init/package.json +1 -1
- package/templates/init/src/lib/file.test.ts +194 -0
- package/templates/init/src/lib/file.ts +113 -0
- package/templates/init/src/suite.ts +14 -0
- package/templates/init/src/types/suite.ts +13 -0
package/README.md
CHANGED
|
@@ -678,6 +678,7 @@ my-project/
|
|
|
678
678
|
basic-auth.ts # base64 Basic auth header builder
|
|
679
679
|
bearer-token.ts # Bearer token header builder
|
|
680
680
|
token.ts # JWT claim decoder
|
|
681
|
+
file.ts # readFile / writeFile / copyFile / moveFile / deleteFile / listDir etc.
|
|
681
682
|
types/
|
|
682
683
|
suite.ts # StepContext, SuiteRunOption string union, auth types -- do not delete sentinel comments
|
|
683
684
|
oauth.ts # OAuthConfig, TokenResponse
|
package/dist/init.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const TEMPLATES_DIR = join(__dirname, '../templates');
|
|
8
|
-
const ROOT_FILES = ['.env.template', 'tsconfig.json', 'tsconfig.test.json', 'jest.config.
|
|
8
|
+
const ROOT_FILES = ['.env.template', 'tsconfig.json', 'tsconfig.test.json', 'jest.config.cjs', 'README.md', 'GUIDE.md'];
|
|
9
9
|
export const initCommand = new Command('init')
|
|
10
10
|
.description('Scaffold a new dtk project in the current directory')
|
|
11
11
|
.argument('[name]', 'project name (defaults to the current directory name)')
|
package/package.json
CHANGED
package/templates/init/GUIDE.md
CHANGED
|
@@ -512,7 +512,9 @@ All shared type definitions: `StepContext`, `StepFn`, `Step`, the `SuiteRunOptio
|
|
|
512
512
|
|
|
513
513
|
### `src/lib/http.ts`
|
|
514
514
|
|
|
515
|
-
Axios wrapper. Provides `httpGet`, `httpPost`, `httpPut`, and `httpDelete`. Normalises errors into plain `Error` objects with readable messages. Use this inside service factories instead of calling axios directly.
|
|
515
|
+
Axios wrapper. Provides `httpGet`, `httpPost`, `httpPut`, and `httpDelete`. Normalises errors into plain `Error` objects with readable messages (`HTTP 404: ...`). Use this inside service factories instead of calling axios directly.
|
|
516
|
+
|
|
517
|
+
All four functions accept an optional `HttpOptions` argument with `headers` and `retry`. When `retry` is set, failed requests are retried up to `attempts` times with either fixed or exponential backoff (`delayMs`, `maxDelayMs`). Provide a `retryOn` predicate to control which errors trigger a retry. `httpDelete` returns the HTTP status code as a `number`.
|
|
516
518
|
|
|
517
519
|
### `src/lib/oauth.ts`
|
|
518
520
|
|
|
@@ -530,6 +532,10 @@ Returns `<prefix> <token>` (e.g. `Bearer sk-abc123`) ready to use as an `Authori
|
|
|
530
532
|
|
|
531
533
|
`getClaimValues(token)` decodes the payload of a JWT and returns the claims as a plain object. Does not verify the signature.
|
|
532
534
|
|
|
535
|
+
### `src/lib/file.ts`
|
|
536
|
+
|
|
537
|
+
File system utilities. Provides `readFile`, `readJson<T>`, `writeFile`, `writeJson`, `appendFile`, `fileExists`, `deleteFile`, `ensureDir`, `copyFile`, `moveFile`, and `listDir`. All functions wrap `node:fs/promises` and normalise errors into readable messages (`file not found`, `permission denied`, `path is a directory`). Parent directories are created automatically on write, append, and copy.
|
|
538
|
+
|
|
533
539
|
### `src/load-env.ts`
|
|
534
540
|
|
|
535
541
|
Loads `.env` then `.env.local` (local overrides). Import this as the very first line of every runbook.
|
package/templates/init/README.md
CHANGED
|
@@ -51,7 +51,7 @@ See `GUIDE.md` for full details.
|
|
|
51
51
|
src/
|
|
52
52
|
suite.ts # core runner -- extend this with custom services
|
|
53
53
|
load-env.ts # dotenv bootstrap -- import first in every runbook
|
|
54
|
-
lib/ # HTTP client, OAuth, auth helpers
|
|
54
|
+
lib/ # HTTP client, OAuth, auth helpers, file system utilities
|
|
55
55
|
types/ # type definitions
|
|
56
56
|
services/ # service factories added by plugins or custom code
|
|
57
57
|
runbooks/ # your runbook scripts
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const config: Config = {
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
const config = {
|
|
4
3
|
preset: 'ts-jest',
|
|
5
4
|
testEnvironment: 'node',
|
|
6
5
|
moduleNameMapper: {
|
|
7
|
-
// source files use .js extensions in imports; map them to .ts for jest
|
|
8
6
|
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
9
7
|
},
|
|
10
8
|
transform: {
|
|
@@ -16,4 +14,4 @@ const config: Config = {
|
|
|
16
14
|
testMatch: ['**/*.test.ts'],
|
|
17
15
|
};
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
module.exports = config;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { readFile, readJson, writeFile, writeJson, appendFile, fileExists, deleteFile, ensureDir, copyFile, moveFile, listDir } from './file.js';
|
|
2
|
+
|
|
3
|
+
jest.mock('node:fs/promises');
|
|
4
|
+
import * as fs from 'node:fs/promises';
|
|
5
|
+
|
|
6
|
+
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.clearAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('readFile', () => {
|
|
13
|
+
it('returns file content as a string', async () => {
|
|
14
|
+
mockFs.readFile.mockResolvedValue('hello world' as any);
|
|
15
|
+
const result = await readFile('/tmp/test.txt');
|
|
16
|
+
expect(result).toBe('hello world');
|
|
17
|
+
expect(mockFs.readFile).toHaveBeenCalledWith('/tmp/test.txt', 'utf-8');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('throws a friendly message on ENOENT', async () => {
|
|
21
|
+
mockFs.readFile.mockRejectedValue(Object.assign(new Error('no such file'), { code: 'ENOENT' }));
|
|
22
|
+
await expect(readFile('/tmp/missing.txt')).rejects.toThrow('file not found: /tmp/missing.txt');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('throws a friendly message on EACCES', async () => {
|
|
26
|
+
mockFs.readFile.mockRejectedValue(Object.assign(new Error('permission denied'), { code: 'EACCES' }));
|
|
27
|
+
await expect(readFile('/tmp/secret.txt')).rejects.toThrow('permission denied: /tmp/secret.txt');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('throws a friendly message on EISDIR', async () => {
|
|
31
|
+
mockFs.readFile.mockRejectedValue(Object.assign(new Error('is a directory'), { code: 'EISDIR' }));
|
|
32
|
+
await expect(readFile('/tmp/adir')).rejects.toThrow('path is a directory: /tmp/adir');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('includes the error code for unrecognised fs errors', async () => {
|
|
36
|
+
mockFs.readFile.mockRejectedValue(Object.assign(new Error('device full'), { code: 'ENOSPC' }));
|
|
37
|
+
await expect(readFile('/tmp/test.txt')).rejects.toThrow('ENOSPC:');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rethrows a plain Error unchanged when no code is present', async () => {
|
|
41
|
+
mockFs.readFile.mockRejectedValue(new Error('unexpected'));
|
|
42
|
+
await expect(readFile('/tmp/test.txt')).rejects.toThrow('unexpected');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('readJson', () => {
|
|
47
|
+
it('parses and returns JSON content', async () => {
|
|
48
|
+
mockFs.readFile.mockResolvedValue('{"id":1,"name":"test"}' as any);
|
|
49
|
+
const result = await readJson<{ id: number; name: string }>('/tmp/data.json');
|
|
50
|
+
expect(result).toEqual({ id: 1, name: 'test' });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('throws when the file contains invalid JSON', async () => {
|
|
54
|
+
mockFs.readFile.mockResolvedValue('not json' as any);
|
|
55
|
+
await expect(readJson('/tmp/bad.json')).rejects.toThrow('invalid JSON in file: /tmp/bad.json');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('propagates read errors from readFile', async () => {
|
|
59
|
+
mockFs.readFile.mockRejectedValue(Object.assign(new Error('no such file'), { code: 'ENOENT' }));
|
|
60
|
+
await expect(readJson('/tmp/missing.json')).rejects.toThrow('file not found: /tmp/missing.json');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('writeFile', () => {
|
|
65
|
+
it('creates the parent directory then writes the content', async () => {
|
|
66
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
67
|
+
mockFs.writeFile.mockResolvedValue(undefined);
|
|
68
|
+
await writeFile('/tmp/dir/test.txt', 'content');
|
|
69
|
+
expect(mockFs.mkdir).toHaveBeenCalledWith('/tmp/dir', { recursive: true });
|
|
70
|
+
expect(mockFs.writeFile).toHaveBeenCalledWith('/tmp/dir/test.txt', 'content', 'utf-8');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('throws a friendly message on write error', async () => {
|
|
74
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
75
|
+
mockFs.writeFile.mockRejectedValue(Object.assign(new Error('permission denied'), { code: 'EACCES' }));
|
|
76
|
+
await expect(writeFile('/tmp/test.txt', 'content')).rejects.toThrow('permission denied: /tmp/test.txt');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('writeJson', () => {
|
|
81
|
+
it('serialises JSON with the default indent of 2', async () => {
|
|
82
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
83
|
+
mockFs.writeFile.mockResolvedValue(undefined);
|
|
84
|
+
await writeJson('/tmp/data.json', { id: 1 });
|
|
85
|
+
expect(mockFs.writeFile).toHaveBeenCalledWith('/tmp/data.json', JSON.stringify({ id: 1 }, null, 2), 'utf-8');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('uses the provided indent value', async () => {
|
|
89
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
90
|
+
mockFs.writeFile.mockResolvedValue(undefined);
|
|
91
|
+
await writeJson('/tmp/data.json', { id: 1 }, 4);
|
|
92
|
+
expect(mockFs.writeFile).toHaveBeenCalledWith('/tmp/data.json', JSON.stringify({ id: 1 }, null, 4), 'utf-8');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('appendFile', () => {
|
|
97
|
+
it('creates the parent directory then appends the content', async () => {
|
|
98
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
99
|
+
mockFs.appendFile.mockResolvedValue(undefined);
|
|
100
|
+
await appendFile('/tmp/dir/log.txt', 'new line\n');
|
|
101
|
+
expect(mockFs.mkdir).toHaveBeenCalledWith('/tmp/dir', { recursive: true });
|
|
102
|
+
expect(mockFs.appendFile).toHaveBeenCalledWith('/tmp/dir/log.txt', 'new line\n', 'utf-8');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('throws a friendly message on append error', async () => {
|
|
106
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
107
|
+
mockFs.appendFile.mockRejectedValue(Object.assign(new Error('permission denied'), { code: 'EACCES' }));
|
|
108
|
+
await expect(appendFile('/tmp/log.txt', 'data')).rejects.toThrow('permission denied: /tmp/log.txt');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('fileExists', () => {
|
|
113
|
+
it('returns true when the file is accessible', async () => {
|
|
114
|
+
mockFs.access.mockResolvedValue(undefined);
|
|
115
|
+
expect(await fileExists('/tmp/exists.txt')).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns false when access throws', async () => {
|
|
119
|
+
mockFs.access.mockRejectedValue(Object.assign(new Error('no such file'), { code: 'ENOENT' }));
|
|
120
|
+
expect(await fileExists('/tmp/missing.txt')).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('deleteFile', () => {
|
|
125
|
+
it('calls rm with force: true', async () => {
|
|
126
|
+
mockFs.rm.mockResolvedValue(undefined);
|
|
127
|
+
await deleteFile('/tmp/old.txt');
|
|
128
|
+
expect(mockFs.rm).toHaveBeenCalledWith('/tmp/old.txt', { force: true });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('throws a friendly message on rm error', async () => {
|
|
132
|
+
mockFs.rm.mockRejectedValue(Object.assign(new Error('permission denied'), { code: 'EACCES' }));
|
|
133
|
+
await expect(deleteFile('/tmp/protected.txt')).rejects.toThrow('permission denied: /tmp/protected.txt');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('ensureDir', () => {
|
|
138
|
+
it('calls mkdir with recursive: true', async () => {
|
|
139
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
140
|
+
await ensureDir('/tmp/new/nested/dir');
|
|
141
|
+
expect(mockFs.mkdir).toHaveBeenCalledWith('/tmp/new/nested/dir', { recursive: true });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('throws a friendly message on mkdir error', async () => {
|
|
145
|
+
mockFs.mkdir.mockRejectedValue(Object.assign(new Error('permission denied'), { code: 'EACCES' }));
|
|
146
|
+
await expect(ensureDir('/tmp/restricted')).rejects.toThrow('permission denied: /tmp/restricted');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('copyFile', () => {
|
|
151
|
+
it('creates the dest directory then copies the file', async () => {
|
|
152
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
153
|
+
mockFs.copyFile.mockResolvedValue(undefined);
|
|
154
|
+
await copyFile('/tmp/src.txt', '/tmp/dest/copy.txt');
|
|
155
|
+
expect(mockFs.mkdir).toHaveBeenCalledWith('/tmp/dest', { recursive: true });
|
|
156
|
+
expect(mockFs.copyFile).toHaveBeenCalledWith('/tmp/src.txt', '/tmp/dest/copy.txt');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('throws a friendly message when the source is not found', async () => {
|
|
160
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
161
|
+
mockFs.copyFile.mockRejectedValue(Object.assign(new Error('no such file'), { code: 'ENOENT' }));
|
|
162
|
+
await expect(copyFile('/tmp/missing.txt', '/tmp/dest.txt')).rejects.toThrow('file not found: /tmp/missing.txt');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('moveFile', () => {
|
|
167
|
+
it('creates the dest directory then renames the file', async () => {
|
|
168
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
169
|
+
mockFs.rename.mockResolvedValue(undefined);
|
|
170
|
+
await moveFile('/tmp/old.txt', '/tmp/moved/new.txt');
|
|
171
|
+
expect(mockFs.mkdir).toHaveBeenCalledWith('/tmp/moved', { recursive: true });
|
|
172
|
+
expect(mockFs.rename).toHaveBeenCalledWith('/tmp/old.txt', '/tmp/moved/new.txt');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('throws a friendly message when the source is not found', async () => {
|
|
176
|
+
mockFs.mkdir.mockResolvedValue(undefined);
|
|
177
|
+
mockFs.rename.mockRejectedValue(Object.assign(new Error('no such file'), { code: 'ENOENT' }));
|
|
178
|
+
await expect(moveFile('/tmp/missing.txt', '/tmp/new.txt')).rejects.toThrow('file not found: /tmp/missing.txt');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('listDir', () => {
|
|
183
|
+
it('returns the directory entries as strings', async () => {
|
|
184
|
+
mockFs.readdir.mockResolvedValue(['a.txt', 'b.txt', 'subdir'] as any);
|
|
185
|
+
const result = await listDir('/tmp/mydir');
|
|
186
|
+
expect(result).toEqual(['a.txt', 'b.txt', 'subdir']);
|
|
187
|
+
expect(mockFs.readdir).toHaveBeenCalledWith('/tmp/mydir');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('throws a friendly message when the directory is not found', async () => {
|
|
191
|
+
mockFs.readdir.mockRejectedValue(Object.assign(new Error('no such file or directory'), { code: 'ENOENT' }));
|
|
192
|
+
await expect(listDir('/tmp/missing')).rejects.toThrow('file not found: /tmp/missing');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFile as fsReadFile,
|
|
3
|
+
writeFile as fsWriteFile,
|
|
4
|
+
appendFile as fsAppendFile,
|
|
5
|
+
rm,
|
|
6
|
+
mkdir,
|
|
7
|
+
copyFile as fsCopyFile,
|
|
8
|
+
rename,
|
|
9
|
+
readdir,
|
|
10
|
+
access,
|
|
11
|
+
} from "node:fs/promises";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
|
|
14
|
+
function normalizeError(err: unknown, path: string): Error {
|
|
15
|
+
if (err instanceof Error && "code" in err) {
|
|
16
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
17
|
+
if (code === "ENOENT") return new Error(`file not found: ${path}`);
|
|
18
|
+
if (code === "EACCES") return new Error(`permission denied: ${path}`);
|
|
19
|
+
if (code === "EISDIR") return new Error(`path is a directory: ${path}`);
|
|
20
|
+
return new Error(`${code}: ${(err as Error).message}`);
|
|
21
|
+
}
|
|
22
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function readFile(path: string): Promise<string> {
|
|
26
|
+
try {
|
|
27
|
+
return await fsReadFile(path, "utf-8");
|
|
28
|
+
} catch (err) {
|
|
29
|
+
throw normalizeError(err, path);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function readJson<T = unknown>(path: string): Promise<T> {
|
|
34
|
+
const content = await readFile(path);
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(content) as T;
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error(`invalid JSON in file: ${path}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function writeFile(path: string, content: string): Promise<void> {
|
|
43
|
+
try {
|
|
44
|
+
await mkdir(dirname(path), { recursive: true });
|
|
45
|
+
await fsWriteFile(path, content, "utf-8");
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw normalizeError(err, path);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function writeJson(path: string, data: unknown, indent = 2): Promise<void> {
|
|
52
|
+
await writeFile(path, JSON.stringify(data, null, indent));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function appendFile(path: string, content: string): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
await mkdir(dirname(path), { recursive: true });
|
|
58
|
+
await fsAppendFile(path, content, "utf-8");
|
|
59
|
+
} catch (err) {
|
|
60
|
+
throw normalizeError(err, path);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function fileExists(path: string): Promise<boolean> {
|
|
65
|
+
try {
|
|
66
|
+
await access(path);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function deleteFile(path: string): Promise<void> {
|
|
74
|
+
try {
|
|
75
|
+
await rm(path, { force: true });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw normalizeError(err, path);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function ensureDir(path: string): Promise<void> {
|
|
82
|
+
try {
|
|
83
|
+
await mkdir(path, { recursive: true });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
throw normalizeError(err, path);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function copyFile(src: string, dest: string): Promise<void> {
|
|
90
|
+
try {
|
|
91
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
92
|
+
await fsCopyFile(src, dest);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
throw normalizeError(err, src);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function moveFile(src: string, dest: string): Promise<void> {
|
|
99
|
+
try {
|
|
100
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
101
|
+
await rename(src, dest);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw normalizeError(err, src);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function listDir(path: string): Promise<string[]> {
|
|
108
|
+
try {
|
|
109
|
+
return await readdir(path);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
throw normalizeError(err, path);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { clientCredentials } from "./lib/oauth.js";
|
|
2
2
|
import { getClaimValues } from "./lib/token.js";
|
|
3
3
|
import { httpGet, httpPost, httpPut, httpDelete } from "./lib/http.js";
|
|
4
|
+
import { readFile, readJson, writeFile, writeJson, appendFile, fileExists, deleteFile, ensureDir, copyFile, moveFile, listDir } from "./lib/file.js";
|
|
4
5
|
import { basicAuth } from "./lib/basic-auth.js";
|
|
5
6
|
import { bearerToken } from "./lib/bearer-token.js";
|
|
6
7
|
// dtk:imports
|
|
@@ -42,6 +43,19 @@ class Suite {
|
|
|
42
43
|
put: httpPut,
|
|
43
44
|
delete: httpDelete,
|
|
44
45
|
},
|
|
46
|
+
file: {
|
|
47
|
+
read: readFile,
|
|
48
|
+
readJson,
|
|
49
|
+
write: writeFile,
|
|
50
|
+
writeJson,
|
|
51
|
+
append: appendFile,
|
|
52
|
+
exists: fileExists,
|
|
53
|
+
delete: deleteFile,
|
|
54
|
+
ensureDir,
|
|
55
|
+
copy: copyFile,
|
|
56
|
+
move: moveFile,
|
|
57
|
+
list: listDir,
|
|
58
|
+
},
|
|
45
59
|
services: {
|
|
46
60
|
// dtk:services
|
|
47
61
|
},
|
|
@@ -22,6 +22,19 @@ export interface StepContext {
|
|
|
22
22
|
put<TBody, TResponse>(url: string, body: TBody, options?: HttpOptions): Promise<TResponse>;
|
|
23
23
|
delete(url: string, options?: HttpOptions): Promise<number>;
|
|
24
24
|
};
|
|
25
|
+
file: {
|
|
26
|
+
read(path: string): Promise<string>;
|
|
27
|
+
readJson<T = unknown>(path: string): Promise<T>;
|
|
28
|
+
write(path: string, content: string): Promise<void>;
|
|
29
|
+
writeJson(path: string, data: unknown, indent?: number): Promise<void>;
|
|
30
|
+
append(path: string, content: string): Promise<void>;
|
|
31
|
+
exists(path: string): Promise<boolean>;
|
|
32
|
+
delete(path: string): Promise<void>;
|
|
33
|
+
ensureDir(path: string): Promise<void>;
|
|
34
|
+
copy(src: string, dest: string): Promise<void>;
|
|
35
|
+
move(src: string, dest: string): Promise<void>;
|
|
36
|
+
list(path: string): Promise<string[]>;
|
|
37
|
+
};
|
|
25
38
|
services: {
|
|
26
39
|
// dtk:service-types
|
|
27
40
|
};
|