codify-plugin-lib 1.0.182-beta4 → 1.0.182-beta40
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/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/plugin/plugin.d.ts +3 -3
- package/dist/plugin/plugin.js +18 -4
- package/dist/pty/background-pty.d.ts +3 -2
- package/dist/pty/background-pty.js +6 -14
- package/dist/pty/index.d.ts +4 -2
- package/dist/pty/seqeuntial-pty.d.ts +3 -2
- package/dist/pty/seqeuntial-pty.js +44 -10
- package/dist/resource/parsed-resource-settings.d.ts +3 -1
- package/dist/resource/parsed-resource-settings.js +15 -2
- package/dist/resource/resource-controller.js +5 -5
- package/dist/resource/resource-settings.d.ts +8 -2
- package/dist/resource/resource-settings.js +2 -2
- package/dist/test.d.ts +1 -0
- package/dist/test.js +5 -0
- package/dist/utils/file-utils.d.ts +23 -0
- package/dist/utils/file-utils.js +186 -0
- package/dist/utils/functions.js +2 -2
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +30 -0
- package/dist/utils/load-resources.d.ts +1 -0
- package/dist/utils/load-resources.js +46 -0
- package/dist/utils/package-json-utils.d.ts +12 -0
- package/dist/utils/package-json-utils.js +34 -0
- package/package.json +4 -3
- package/src/index.ts +2 -1
- package/src/plugin/plugin.test.ts +31 -0
- package/src/plugin/plugin.ts +21 -4
- package/src/pty/background-pty.ts +9 -18
- package/src/pty/index.ts +6 -4
- package/src/pty/seqeuntial-pty.ts +59 -14
- package/src/pty/sequential-pty.test.ts +138 -5
- package/src/resource/parsed-resource-settings.test.ts +24 -0
- package/src/resource/parsed-resource-settings.ts +23 -7
- package/src/resource/resource-controller.test.ts +126 -0
- package/src/resource/resource-controller.ts +5 -6
- package/src/resource/resource-settings.test.ts +36 -0
- package/src/resource/resource-settings.ts +11 -4
- package/src/utils/file-utils.test.ts +7 -0
- package/src/utils/file-utils.ts +231 -0
- package/src/utils/functions.ts +3 -3
- package/src/utils/index.ts +37 -0
- package/src/utils/internal-utils.test.ts +1 -0
- package/src/utils/load-resources.ts +53 -0
- package/src/utils/package-json-utils.ts +40 -0
|
@@ -2,6 +2,7 @@ import { JSONSchemaType } from 'ajv';
|
|
|
2
2
|
import { OS, StringIndexedObject } from 'codify-schemas';
|
|
3
3
|
import isObjectsEqual from 'lodash.isequal'
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { ZodObject } from 'zod';
|
|
5
6
|
|
|
6
7
|
import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js';
|
|
7
8
|
import {
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
tildify,
|
|
12
13
|
untildify
|
|
13
14
|
} from '../utils/functions.js';
|
|
15
|
+
import { ParsedResourceSettings } from './parsed-resource-settings.js';
|
|
14
16
|
import { RefreshContext } from './resource.js';
|
|
15
17
|
|
|
16
18
|
export interface InputTransformation {
|
|
@@ -36,7 +38,7 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
36
38
|
/**
|
|
37
39
|
* Schema to validate user configs with. Must be in the format JSON Schema draft07
|
|
38
40
|
*/
|
|
39
|
-
schema?: Partial<JSONSchemaType<T | any
|
|
41
|
+
schema?: Partial<JSONSchemaType<T | any>> | ZodObject;
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* Mark the resource as sensitive. Defaults to false. This prevents the resource from automatically being imported by init and import.
|
|
@@ -44,6 +46,11 @@ export interface ResourceSettings<T extends StringIndexedObject> {
|
|
|
44
46
|
*/
|
|
45
47
|
isSensitive?: boolean;
|
|
46
48
|
|
|
49
|
+
/**
|
|
50
|
+
* An optional description of the resource. This does not affect the behavior of the resource.
|
|
51
|
+
*/
|
|
52
|
+
description?: string;
|
|
53
|
+
|
|
47
54
|
/**
|
|
48
55
|
* Allow multiple of the same resource to unique. Set truthy if
|
|
49
56
|
* multiples are allowed, for example for applications, there can be multiple copy of the same application installed
|
|
@@ -349,7 +356,7 @@ export interface StatefulParameterSetting extends DefaultParameterSetting {
|
|
|
349
356
|
|
|
350
357
|
const ParameterEqualsDefaults: Partial<Record<ParameterSettingType, (a: unknown, b: unknown) => boolean>> = {
|
|
351
358
|
'boolean': (a: unknown, b: unknown) => Boolean(a) === Boolean(b),
|
|
352
|
-
'directory'
|
|
359
|
+
'directory'(a: unknown, b: unknown) {
|
|
353
360
|
let transformedA = resolvePathWithVariables(untildify(String(a)))
|
|
354
361
|
let transformedB = resolvePathWithVariables(untildify(String(b)))
|
|
355
362
|
|
|
@@ -430,7 +437,7 @@ export function resolveFnFromEqualsFnOrString(
|
|
|
430
437
|
const ParameterTransformationDefaults: Partial<Record<ParameterSettingType, InputTransformation>> = {
|
|
431
438
|
'directory': {
|
|
432
439
|
to: (a: unknown) => resolvePathWithVariables((untildify(String(a)))),
|
|
433
|
-
from
|
|
440
|
+
from(a: unknown, original) {
|
|
434
441
|
if (ParameterEqualsDefaults.directory!(a, original)) {
|
|
435
442
|
return original;
|
|
436
443
|
}
|
|
@@ -489,7 +496,7 @@ export function resolveParameterTransformFn(
|
|
|
489
496
|
}
|
|
490
497
|
|
|
491
498
|
export function resolveMatcher<T extends StringIndexedObject>(
|
|
492
|
-
settings:
|
|
499
|
+
settings: ParsedResourceSettings<T>
|
|
493
500
|
): (desired: Partial<T>, current: Partial<T>) => boolean {
|
|
494
501
|
return typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple?.matcher
|
|
495
502
|
? ((desired: Partial<T>, current: Partial<T>) => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { describe, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
describe('File utils tests', { timeout: 100_000_000 }, () => {
|
|
4
|
+
it('Can download a file', async () => {
|
|
5
|
+
// await FileUtils.downloadFile('https://download.jetbrains.com/webstorm/WebStorm-2025.3.1-aarch64.dmg?_gl=1*1huoi7o*_gcl_aw*R0NMLjE3NjU3NDAwMTcuQ2p3S0NBaUEzZm5KQmhBZ0Vpd0F5cW1ZNVhLVENlbHJOcTk2YXdjZVlfMS1wdE91MXc0WDk2bFJkVDM3QURhUFNJMUtwNVVSVUhxWTJob0NuZ0FRQXZEX0J3RQ..*_gcl_au*MjA0MDQ0MjE2My4xNzYzNjQzNzMz*FPAU*MjA0MDQ0MjE2My4xNzYzNjQzNzMz*_ga*MTYxMDg4MTkzMi4xNzYzNjQzNzMz*_ga_9J976DJZ68*czE3NjYzNjI5ODAkbzEyJGcxJHQxNzY2MzYzMDQwJGo2MCRsMCRoMA..', path.join(process.cwd(), 'google.html'));
|
|
6
|
+
})
|
|
7
|
+
})
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import * as fsSync from 'node:fs';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { finished } from 'node:stream/promises';
|
|
6
|
+
|
|
7
|
+
import { Utils } from './index.js';
|
|
8
|
+
|
|
9
|
+
const SPACE_REGEX = /^\s*$/
|
|
10
|
+
|
|
11
|
+
export class FileUtils {
|
|
12
|
+
static async downloadFile(url: string, destination: string): Promise<void> {
|
|
13
|
+
console.log(`Downloading file from ${url} to ${destination}`);
|
|
14
|
+
const { body } = await fetch(url)
|
|
15
|
+
|
|
16
|
+
const dirname = path.dirname(destination);
|
|
17
|
+
if (!await fs.stat(dirname).then((s) => s.isDirectory()).catch(() => false)) {
|
|
18
|
+
await fs.mkdir(dirname, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ws = fsSync.createWriteStream(destination)
|
|
22
|
+
// Different type definitions here for readable stream (NodeJS vs DOM). Small hack to fix that
|
|
23
|
+
await finished(Readable.fromWeb(body as never).pipe(ws));
|
|
24
|
+
|
|
25
|
+
console.log(`Finished downloading to ${destination}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static async addToShellRc(line: string): Promise<void> {
|
|
29
|
+
const lineToInsert = addLeadingSpacer(
|
|
30
|
+
addTrailingSpacer(line)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
await fs.appendFile(Utils.getPrimaryShellRc(), lineToInsert)
|
|
34
|
+
|
|
35
|
+
function addLeadingSpacer(line: string): string {
|
|
36
|
+
return line.startsWith('\n')
|
|
37
|
+
? line
|
|
38
|
+
: '\n' + line;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function addTrailingSpacer(line: string): string {
|
|
42
|
+
return line.endsWith('\n')
|
|
43
|
+
? line
|
|
44
|
+
: line + '\n';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static async addAllToShellRc(lines: string[]): Promise<void> {
|
|
49
|
+
const formattedLines = '\n' + lines.join('\n') + '\n';
|
|
50
|
+
const shellRc = Utils.getPrimaryShellRc();
|
|
51
|
+
|
|
52
|
+
console.log(`Adding to ${path.basename(shellRc)}:
|
|
53
|
+
${lines.join('\n')}`)
|
|
54
|
+
|
|
55
|
+
await fs.appendFile(shellRc, formattedLines)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* This method adds a directory path to the shell rc file if it doesn't already exist.
|
|
60
|
+
*
|
|
61
|
+
* @param value - The directory path to add.
|
|
62
|
+
* @param prepend - Whether to prepend the path to the existing PATH variable.
|
|
63
|
+
*/
|
|
64
|
+
static async addPathToShellRc(value: string, prepend: boolean): Promise<void> {
|
|
65
|
+
if (await Utils.isDirectoryOnPath(value)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const shellRc = Utils.getPrimaryShellRc();
|
|
70
|
+
console.log(`Saving path: ${value} to ${shellRc}`);
|
|
71
|
+
|
|
72
|
+
if (prepend) {
|
|
73
|
+
await fs.appendFile(shellRc, `\nexport PATH=$PATH:${value};`, { encoding: 'utf8' });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await fs.appendFile(shellRc, `\nexport PATH=${value}:$PATH;`, { encoding: 'utf8' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static async removeFromFile(filePath: string, search: string): Promise<void> {
|
|
81
|
+
const contents = await fs.readFile(filePath, 'utf8');
|
|
82
|
+
const newContents = contents.replaceAll(search, '');
|
|
83
|
+
|
|
84
|
+
await fs.writeFile(filePath, newContents, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static async removeLineFromFile(filePath: string, search: RegExp | string): Promise<void> {
|
|
88
|
+
const file = await fs.readFile(filePath, 'utf8')
|
|
89
|
+
const lines = file.split('\n');
|
|
90
|
+
|
|
91
|
+
let searchRegex;
|
|
92
|
+
let searchString;
|
|
93
|
+
|
|
94
|
+
if (typeof search === 'object') {
|
|
95
|
+
const startRegex = /^([\t ]*)?/;
|
|
96
|
+
const endRegex = /([\t ]*)?/;
|
|
97
|
+
|
|
98
|
+
// Augment regex with spaces criteria to make sure this function is not deleting lines that are comments or has other content.
|
|
99
|
+
searchRegex = search
|
|
100
|
+
? new RegExp(
|
|
101
|
+
startRegex.source + search.source + endRegex.source,
|
|
102
|
+
search.flags
|
|
103
|
+
)
|
|
104
|
+
: search;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (typeof search === 'string') {
|
|
108
|
+
searchString = search;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (let counter = lines.length; counter >= 0; counter--) {
|
|
112
|
+
if (!lines[counter]) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (searchString && lines[counter].includes(searchString)) {
|
|
117
|
+
lines.splice(counter, 1);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (searchRegex && lines[counter].search(searchRegex) !== -1) {
|
|
122
|
+
lines.splice(counter, 1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await fs.writeFile(filePath, lines.join('\n'));
|
|
127
|
+
console.log(`Removed line: ${search} from ${filePath}`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static async removeLineFromShellRc(search: RegExp | string): Promise<void> {
|
|
131
|
+
return FileUtils.removeLineFromFile(Utils.getPrimaryShellRc(), search);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static async removeAllLinesFromShellRc(searches: Array<RegExp | string>): Promise<void> {
|
|
135
|
+
for (const search of searches) {
|
|
136
|
+
await FileUtils.removeLineFromFile(Utils.getPrimaryShellRc(), search);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Append the string to the end of a file ensuring at least 1 lines of space between.
|
|
141
|
+
// Ex result:
|
|
142
|
+
// something something;
|
|
143
|
+
//
|
|
144
|
+
// newline;
|
|
145
|
+
static appendToFileWithSpacing(file: string, textToInsert: string): string {
|
|
146
|
+
const lines = file.trimEnd().split(/\n/);
|
|
147
|
+
if (lines.length === 0) {
|
|
148
|
+
return textToInsert;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const endingNewLines = FileUtils.calculateEndingNewLines(lines);
|
|
152
|
+
const numNewLines = endingNewLines === -1
|
|
153
|
+
? 0
|
|
154
|
+
: Math.max(0, 2 - endingNewLines);
|
|
155
|
+
return lines.join('\n') + '\n'.repeat(numNewLines) + textToInsert
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static async dirExists(path: string): Promise<boolean> {
|
|
159
|
+
let stat;
|
|
160
|
+
try {
|
|
161
|
+
stat = await fs.stat(path);
|
|
162
|
+
return stat.isDirectory();
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static async fileExists(path: string): Promise<boolean> {
|
|
169
|
+
let stat;
|
|
170
|
+
try {
|
|
171
|
+
stat = await fs.stat(path);
|
|
172
|
+
return stat.isFile();
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static async exists(path: string): Promise<boolean> {
|
|
179
|
+
try {
|
|
180
|
+
await fs.stat(path);
|
|
181
|
+
return true;
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static async checkDirExistsOrThrowIfFile(path: string): Promise<boolean> {
|
|
188
|
+
let stat;
|
|
189
|
+
try {
|
|
190
|
+
stat = await fs.stat(path);
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (stat.isDirectory()) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
throw new Error(`Directory ${path} already exists and is a file`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
static async createDirIfNotExists(path: string): Promise<void> {
|
|
203
|
+
if (!fsSync.existsSync(path)) {
|
|
204
|
+
await fs.mkdir(path, { recursive: true });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// This is overly complicated but it can be used to insert into any
|
|
209
|
+
// position in the future
|
|
210
|
+
private static calculateEndingNewLines(lines: string[]): number {
|
|
211
|
+
let counter = 0;
|
|
212
|
+
while (true) {
|
|
213
|
+
const line = lines.at(-counter - 1);
|
|
214
|
+
|
|
215
|
+
if (!line) {
|
|
216
|
+
return -1
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!SPACE_REGEX.test(line)) {
|
|
220
|
+
return counter;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
counter++;
|
|
224
|
+
|
|
225
|
+
// Short circuit here because we don't need to check over 2;
|
|
226
|
+
if (counter > 2) {
|
|
227
|
+
return counter;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
package/src/utils/functions.ts
CHANGED
|
@@ -9,10 +9,11 @@ export function splitUserConfig<T extends StringIndexedObject>(
|
|
|
9
9
|
type: config.type,
|
|
10
10
|
...(config.name ? { name: config.name } : {}),
|
|
11
11
|
...(config.dependsOn ? { dependsOn: config.dependsOn } : {}),
|
|
12
|
+
...(config.os ? { os: config.os } : {}),
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
15
|
-
const { type, name, dependsOn, ...parameters } = config;
|
|
16
|
+
const { type, name, dependsOn, os, ...parameters } = config;
|
|
16
17
|
|
|
17
18
|
return {
|
|
18
19
|
parameters: parameters as T,
|
|
@@ -35,8 +36,7 @@ export function tildify(pathWithTilde: string) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export function resolvePathWithVariables(pathWithVariables: string) {
|
|
38
|
-
|
|
39
|
-
return pathWithVariables.replace(/\$([A-Z_]+[A-Z0-9_]*)|\${([A-Z0-9_]*)}/ig, (_, a, b) => process.env[a || b])
|
|
39
|
+
return pathWithVariables.replace(/\$([A-Z_]+[A-Z0-9_]*)|\${([A-Z0-9_]*)}/ig, (_, a, b) => process.env[a || b]!)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function addVariablesToPath(pathWithoutVariables: string) {
|
package/src/utils/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { OS } from 'codify-schemas';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
+
import { getPty, SpawnStatus } from '../pty/index.js';
|
|
6
|
+
|
|
5
7
|
export function isDebug(): boolean {
|
|
6
8
|
return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library
|
|
7
9
|
}
|
|
@@ -40,6 +42,34 @@ export const Utils = {
|
|
|
40
42
|
return os.platform() === 'linux';
|
|
41
43
|
},
|
|
42
44
|
|
|
45
|
+
async isArmArch(): Promise<boolean> {
|
|
46
|
+
const $ = getPty();
|
|
47
|
+
if (!Utils.isMacOS()) {
|
|
48
|
+
// On Linux, check uname -m
|
|
49
|
+
const query = await $.spawn('uname -m');
|
|
50
|
+
return query.data.trim() === 'aarch64' || query.data.trim() === 'arm64';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const query = await $.spawn('sysctl -n machdep.cpu.brand_string');
|
|
54
|
+
return /M(\d)/.test(query.data);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async isHomebrewInstalled(): Promise<boolean> {
|
|
58
|
+
const $ = getPty();
|
|
59
|
+
const query = await $.spawnSafe('which brew', { interactive: true });
|
|
60
|
+
return query.status === SpawnStatus.SUCCESS;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async isRosetta2Installed(): Promise<boolean> {
|
|
64
|
+
if (!Utils.isMacOS()) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const $ = getPty();
|
|
69
|
+
const query = await $.spawnSafe('arch -x86_64 /usr/bin/true 2> /dev/null', { interactive: true });
|
|
70
|
+
return query.status === SpawnStatus.SUCCESS;
|
|
71
|
+
},
|
|
72
|
+
|
|
43
73
|
getShell(): Shell | undefined {
|
|
44
74
|
const shell = process.env.SHELL || '';
|
|
45
75
|
|
|
@@ -138,6 +168,13 @@ export const Utils = {
|
|
|
138
168
|
path.join(homeDir, '.profile'),
|
|
139
169
|
];
|
|
140
170
|
},
|
|
171
|
+
|
|
172
|
+
async isDirectoryOnPath(directory: string): Promise<boolean> {
|
|
173
|
+
const $ = getPty();
|
|
174
|
+
const { data: pathQuery } = await $.spawn('echo $PATH', { interactive: true });
|
|
175
|
+
const lines = pathQuery.split(':');
|
|
176
|
+
return lines.includes(directory);
|
|
177
|
+
},
|
|
141
178
|
};
|
|
142
179
|
|
|
143
180
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import * as url from 'node:url';
|
|
4
|
+
|
|
5
|
+
export const listAllResources = async (root = path.join(path.dirname(url.fileURLToPath(import.meta.url)), '..', '..', '..', '..')) => {
|
|
6
|
+
console.log('Dirname', root);
|
|
7
|
+
|
|
8
|
+
const resourcesPath = path.join(root, 'src', 'resources')
|
|
9
|
+
|
|
10
|
+
const resourceDir = await fs.readdir(resourcesPath);
|
|
11
|
+
const dedupSet = new Set();
|
|
12
|
+
const result = new Set<string>();
|
|
13
|
+
|
|
14
|
+
for (const folder of resourceDir) {
|
|
15
|
+
if (await fs.stat(path.join(resourcesPath, folder)).then(s => s.isDirectory()).catch(() => false)) {
|
|
16
|
+
for (const folderContents of await fs.readdir(path.join(resourcesPath, folder))) {
|
|
17
|
+
const isDirectory = await fs.stat(path.join(resourcesPath, folder, folderContents)).then(s => s.isDirectory());
|
|
18
|
+
|
|
19
|
+
// console.log(folderContents, isDirectory);
|
|
20
|
+
if (isDirectory) {
|
|
21
|
+
for (const innerContents of await fs.readdir(path.join(resourcesPath, folder, folderContents))) {
|
|
22
|
+
if (!dedupSet.has(path.join(resourcesPath, folder, folderContents))) {
|
|
23
|
+
dedupSet.add(path.join(resourcesPath, folder, folderContents));
|
|
24
|
+
addResourceFromDir(path.join(resourcesPath, folder,folderContents), result);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
if (!dedupSet.has(path.join(resourcesPath, folder))) {
|
|
29
|
+
dedupSet.add(path.join(resourcesPath, folder));
|
|
30
|
+
addResourceFromDir(path.join(resourcesPath, folder), result);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error('Only directories are allowed in resources folder')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [...result];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
function addResourceFromDir(dir: string, result: Set<string>): void {
|
|
45
|
+
try {
|
|
46
|
+
const resourceFile = path.resolve(path.join(dir, 'resource.ts'));
|
|
47
|
+
if (!(fs.stat(resourceFile).then((s) => s.isFile())).catch(() => false)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
result.add(resourceFile);
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find the nearest package.json starting from a directory and walking upward.
|
|
6
|
+
* @param {string} startDir - Directory to start searching from
|
|
7
|
+
* @returns {string|null} Absolute path to package.json or null if not found
|
|
8
|
+
*/
|
|
9
|
+
export function findNearestPackageJson(startDir = process.cwd()) {
|
|
10
|
+
let currentDir = path.resolve(startDir);
|
|
11
|
+
|
|
12
|
+
while (true) {
|
|
13
|
+
const pkgPath = path.join(currentDir, "package.json");
|
|
14
|
+
|
|
15
|
+
if (fs.existsSync(pkgPath)) {
|
|
16
|
+
return pkgPath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const parentDir = path.dirname(currentDir);
|
|
20
|
+
if (parentDir === currentDir) {
|
|
21
|
+
// Reached filesystem root
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
currentDir = parentDir;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read and parse the nearest package.json
|
|
31
|
+
* @param {string} startDir
|
|
32
|
+
* @returns {object|null}
|
|
33
|
+
*/
|
|
34
|
+
export function readNearestPackageJson(startDir: string = process.cwd()) {
|
|
35
|
+
const pkgPath = findNearestPackageJson(startDir);
|
|
36
|
+
if (!pkgPath) return null;
|
|
37
|
+
|
|
38
|
+
const contents = fs.readFileSync(pkgPath, 'utf8');
|
|
39
|
+
return JSON.parse(contents);
|
|
40
|
+
}
|