@nosto/nosto-cli 1.0.1
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/.github/copilot-instructions.md +326 -0
- package/.github/dependabot.yml +9 -0
- package/.github/pull_request_template.md +12 -0
- package/.github/workflows/ci.yml +58 -0
- package/.github/workflows/release.yml +49 -0
- package/.husky/commit-msg +1 -0
- package/.prettierrc +9 -0
- package/LICENSE +29 -0
- package/README.md +154 -0
- package/commitlint.config.js +4 -0
- package/eslint.config.js +36 -0
- package/package.json +63 -0
- package/src/api/library/fetchLibraryFile.ts +18 -0
- package/src/api/retry.ts +28 -0
- package/src/api/source/fetchSourceFile.ts +33 -0
- package/src/api/source/listSourceFiles.ts +13 -0
- package/src/api/source/putSourceFile.ts +14 -0
- package/src/api/source/schema.ts +10 -0
- package/src/api/utils.ts +52 -0
- package/src/bootstrap.sh +26 -0
- package/src/commander.ts +119 -0
- package/src/config/authConfig.ts +42 -0
- package/src/config/config.ts +109 -0
- package/src/config/envConfig.ts +23 -0
- package/src/config/fileConfig.ts +39 -0
- package/src/config/schema.ts +70 -0
- package/src/config/searchTemplatesConfig.ts +33 -0
- package/src/console/logger.ts +93 -0
- package/src/console/userPrompt.ts +16 -0
- package/src/errors/InvalidLoginResponseError.ts +14 -0
- package/src/errors/MissingConfigurationError.ts +14 -0
- package/src/errors/NostoError.ts +13 -0
- package/src/errors/NotNostoTemplateError.ts +15 -0
- package/src/errors/withErrorHandler.ts +50 -0
- package/src/exports.ts +8 -0
- package/src/filesystem/asserts/assertGitRepo.ts +19 -0
- package/src/filesystem/asserts/assertNostoTemplate.ts +34 -0
- package/src/filesystem/calculateTreeHash.ts +28 -0
- package/src/filesystem/esbuild.ts +37 -0
- package/src/filesystem/esbuildPlugins.ts +72 -0
- package/src/filesystem/filesystem.ts +40 -0
- package/src/filesystem/isIgnored.ts +65 -0
- package/src/filesystem/legacyUtils.ts +10 -0
- package/src/filesystem/loadLibrary.ts +31 -0
- package/src/filesystem/processInBatches.ts +38 -0
- package/src/filesystem/utils/getLoaderScript.ts +28 -0
- package/src/index.ts +3 -0
- package/src/modules/login.ts +87 -0
- package/src/modules/logout.ts +13 -0
- package/src/modules/search-templates/build.ts +61 -0
- package/src/modules/search-templates/dev.ts +50 -0
- package/src/modules/search-templates/pull.ts +89 -0
- package/src/modules/search-templates/push.ts +121 -0
- package/src/modules/setup.ts +96 -0
- package/src/modules/status.ts +71 -0
- package/src/utils/withSafeEnvironment.ts +22 -0
- package/test/api/fetchSourceFile.test.ts +30 -0
- package/test/api/putSourceFile.test.ts +34 -0
- package/test/api/retry.test.ts +102 -0
- package/test/api/utils.test.ts +27 -0
- package/test/commander.test.ts +271 -0
- package/test/config/envConfig.test.ts +62 -0
- package/test/config/fileConfig.test.ts +63 -0
- package/test/config/schema.test.ts +96 -0
- package/test/config/searchTemplatesConfig.test.ts +43 -0
- package/test/console/logger.test.ts +96 -0
- package/test/errors/withErrorHandler.test.ts +64 -0
- package/test/filesystem/filesystem.test.ts +53 -0
- package/test/filesystem/plugins.test.ts +35 -0
- package/test/index.test.ts +15 -0
- package/test/modules/search-templates/build.legacy.test.ts +74 -0
- package/test/modules/search-templates/build.modern.test.ts +33 -0
- package/test/modules/search-templates/dev.legacy.test.ts +75 -0
- package/test/modules/search-templates/dev.modern.test.ts +44 -0
- package/test/modules/search-templates/pull.test.ts +96 -0
- package/test/modules/search-templates/push.test.ts +109 -0
- package/test/modules/setup.test.ts +49 -0
- package/test/modules/status.test.ts +22 -0
- package/test/setup.ts +28 -0
- package/test/utils/generateEndpointMock.ts +60 -0
- package/test/utils/mockCommander.ts +22 -0
- package/test/utils/mockConfig.ts +37 -0
- package/test/utils/mockConsole.ts +65 -0
- package/test/utils/mockFileSystem.ts +52 -0
- package/test/utils/mockServer.ts +76 -0
- package/test/utils/mockStarterManifest.ts +42 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +33 -0
package/eslint.config.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import js from "@eslint/js"
|
|
2
|
+
import { defineConfig } from "eslint/config"
|
|
3
|
+
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
|
|
4
|
+
import simpleImportSort from "eslint-plugin-simple-import-sort"
|
|
5
|
+
import unusedImports from "eslint-plugin-unused-imports"
|
|
6
|
+
import tseslint from "typescript-eslint"
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
{
|
|
10
|
+
files: ["**/*.ts"],
|
|
11
|
+
plugins: {
|
|
12
|
+
js,
|
|
13
|
+
"@typescript-eslint": tseslint.plugin
|
|
14
|
+
},
|
|
15
|
+
extends: ["js/recommended", ...tseslint.configs.recommended],
|
|
16
|
+
languageOptions: {
|
|
17
|
+
parser: tseslint.parser,
|
|
18
|
+
parserOptions: {
|
|
19
|
+
project: "./tsconfig.json"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
plugins: {
|
|
25
|
+
"unused-imports": unusedImports,
|
|
26
|
+
"simple-import-sort": simpleImportSort
|
|
27
|
+
},
|
|
28
|
+
rules: {
|
|
29
|
+
"unused-imports/no-unused-imports": "error",
|
|
30
|
+
"simple-import-sort/imports": "error",
|
|
31
|
+
"simple-import-sort/exports": "error",
|
|
32
|
+
"no-restricted-imports": ["error", "node:test"]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
eslintPluginPrettierRecommended
|
|
36
|
+
])
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nosto/nosto-cli",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"main": "./src/index.ts",
|
|
5
|
+
"bin": {
|
|
6
|
+
"nosto": "./src/bootstrap.sh"
|
|
7
|
+
},
|
|
8
|
+
"author": "Nosto",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"imports": {
|
|
11
|
+
"#*": "./src/*"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./src/exports.ts"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=24.0.0"
|
|
18
|
+
},
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"description": "",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"lint": "eslint",
|
|
23
|
+
"lint:fix": "eslint --fix",
|
|
24
|
+
"prepare": "husky",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"test:ui": "vitest --ui",
|
|
28
|
+
"test:coverage": "vitest run --coverage",
|
|
29
|
+
"type-check": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chalk": "^5.6.2",
|
|
33
|
+
"commander": "^14.0.1",
|
|
34
|
+
"esbuild": "^0.25.10",
|
|
35
|
+
"ignore": "^7.0.5",
|
|
36
|
+
"open": "^10.2.0",
|
|
37
|
+
"ky": "^1.11.0",
|
|
38
|
+
"preact": "^10.27.2",
|
|
39
|
+
"zod": "^4.1.11"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@commitlint/cli": "^20.1.0",
|
|
43
|
+
"@commitlint/config-conventional": "^20.0.0",
|
|
44
|
+
"@eslint/js": "^9.37.0",
|
|
45
|
+
"@types/node": "^24.7.0",
|
|
46
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
47
|
+
"@vitest/ui": "^3.2.4",
|
|
48
|
+
"eslint": "^9.37.0",
|
|
49
|
+
"eslint-config-prettier": "^10.1.8",
|
|
50
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
51
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
52
|
+
"eslint-plugin-unused-imports": "^4.2.0",
|
|
53
|
+
"husky": "^9.1.7",
|
|
54
|
+
"memfs": "^4.48.1",
|
|
55
|
+
"msw": "^2.11.3",
|
|
56
|
+
"prettier": "^3.6.2",
|
|
57
|
+
"typescript-eslint": "^8.45.0",
|
|
58
|
+
"vitest": "^3.2.4"
|
|
59
|
+
},
|
|
60
|
+
"publishConfig": {
|
|
61
|
+
"access": "public"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import ky from "ky"
|
|
2
|
+
import z from "zod"
|
|
3
|
+
|
|
4
|
+
import { cleanUrl, getJsonHeaders } from "#api/utils.ts"
|
|
5
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
6
|
+
|
|
7
|
+
const FetchSourceFileSchema = z.string()
|
|
8
|
+
|
|
9
|
+
export async function fetchLibraryFile(path: string) {
|
|
10
|
+
const config = getCachedConfig()
|
|
11
|
+
const url = `${config.libraryUrl}/${cleanUrl(path)}`
|
|
12
|
+
|
|
13
|
+
const response = await ky.get(url.toString(), {
|
|
14
|
+
headers: getJsonHeaders()
|
|
15
|
+
})
|
|
16
|
+
const data = FetchSourceFileSchema.parse(await response.text())
|
|
17
|
+
return data
|
|
18
|
+
}
|
package/src/api/retry.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
|
|
3
|
+
import { Logger } from "#console/logger.ts"
|
|
4
|
+
|
|
5
|
+
const MAX_RETRIES = 3
|
|
6
|
+
const INITIAL_RETRY_DELAY = 1000 // 1 second
|
|
7
|
+
|
|
8
|
+
export async function fetchWithRetry(
|
|
9
|
+
apiFunction: (filePath: string) => Promise<string>,
|
|
10
|
+
filePath: string,
|
|
11
|
+
retryCount = 0
|
|
12
|
+
): Promise<string> {
|
|
13
|
+
try {
|
|
14
|
+
return await apiFunction(filePath)
|
|
15
|
+
} catch (error: unknown) {
|
|
16
|
+
if (retryCount >= MAX_RETRIES) {
|
|
17
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
18
|
+
Logger.error(`${chalk.red("✗")} ${chalk.cyan(filePath)}: ${errorMessage}`)
|
|
19
|
+
throw new Error(`Failed to fetch ${filePath} after ${MAX_RETRIES} retries: ${errorMessage}`)
|
|
20
|
+
}
|
|
21
|
+
const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount)
|
|
22
|
+
Logger.warn(
|
|
23
|
+
`${chalk.yellow("⟳")} Failed to fetch ${chalk.cyan(filePath)}: Retrying in ${delay}ms (attempt ${retryCount + 1}/${MAX_RETRIES})`
|
|
24
|
+
)
|
|
25
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
26
|
+
return fetchWithRetry(apiFunction, filePath, retryCount + 1)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import ky from "ky"
|
|
2
|
+
import z from "zod"
|
|
3
|
+
|
|
4
|
+
import { getJsonHeaders, getSourceUrl } from "#api/utils.ts"
|
|
5
|
+
|
|
6
|
+
const FetchSourceFileSchema = z.string()
|
|
7
|
+
|
|
8
|
+
export async function fetchSourceFile(path: string) {
|
|
9
|
+
const response = await ky.get(getSourceUrl(`source/{env}/${path}`), {
|
|
10
|
+
headers: getJsonHeaders()
|
|
11
|
+
})
|
|
12
|
+
const data = FetchSourceFileSchema.parse(await response.text())
|
|
13
|
+
return data
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const KyErrorSchema = z.object({
|
|
17
|
+
response: z.object({
|
|
18
|
+
status: z.number(),
|
|
19
|
+
statusText: z.string()
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export async function fetchSourceFileIfExists(path: string) {
|
|
24
|
+
try {
|
|
25
|
+
return await fetchSourceFile(path)
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const parsedError = KyErrorSchema.safeParse(error)
|
|
28
|
+
if (parsedError.success && parsedError.data.response.status === 404) {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
throw error
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import ky from "ky"
|
|
2
|
+
|
|
3
|
+
import { getJsonHeaders, getSourceUrl } from "#api/utils.ts"
|
|
4
|
+
|
|
5
|
+
import { ListSourceFilesSchema } from "./schema.ts"
|
|
6
|
+
|
|
7
|
+
export async function listSourceFiles() {
|
|
8
|
+
const response = await ky.get(getSourceUrl("source/{env}"), {
|
|
9
|
+
headers: getJsonHeaders()
|
|
10
|
+
})
|
|
11
|
+
const files = ListSourceFilesSchema.parse(await response.json())
|
|
12
|
+
return files.filter(file => file.path)
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import ky from "ky"
|
|
2
|
+
|
|
3
|
+
import { getHeaders, getSourceUrl } from "#api/utils.ts"
|
|
4
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
5
|
+
|
|
6
|
+
export async function putSourceFile(path: string, data: string) {
|
|
7
|
+
if (getCachedConfig().dryRun) {
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
await ky.put(getSourceUrl(`source/{env}/${path}`), {
|
|
11
|
+
headers: getHeaders(),
|
|
12
|
+
body: data
|
|
13
|
+
})
|
|
14
|
+
}
|
package/src/api/utils.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getCachedConfig } from "#config/config.ts"
|
|
2
|
+
|
|
3
|
+
export function getSourceUrl(path: string) {
|
|
4
|
+
const config = getCachedConfig()
|
|
5
|
+
const merchant = config.merchant
|
|
6
|
+
const env = config.templatesEnv
|
|
7
|
+
const replacedPath = path.replace("{env}", env)
|
|
8
|
+
return `${config.apiUrl}/${merchant}/search-templates/${replacedPath}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getHeaders() {
|
|
12
|
+
return new Headers({
|
|
13
|
+
"Content-Type": "application/octet-stream",
|
|
14
|
+
...getAuthHeaders()
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getJsonHeaders() {
|
|
19
|
+
return new Headers({
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
...getAuthHeaders()
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type AuthHeaders =
|
|
26
|
+
| {
|
|
27
|
+
Authorization: string
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
"X-Nosto-User": string
|
|
31
|
+
"X-Nosto-Token": string
|
|
32
|
+
"X-Nosto-Merchant": string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getAuthHeaders(): AuthHeaders {
|
|
36
|
+
const config = getCachedConfig()
|
|
37
|
+
if (config.apiKey) {
|
|
38
|
+
return {
|
|
39
|
+
Authorization: "Basic " + btoa(":" + config.apiKey)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
"X-Nosto-User": config.auth.user,
|
|
44
|
+
"X-Nosto-Token": config.auth.token,
|
|
45
|
+
"X-Nosto-Merchant": config.merchant
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Remove trailing and leading slashes
|
|
50
|
+
export function cleanUrl(url: string) {
|
|
51
|
+
return url.replace(/^\/+|\/+$/g, "")
|
|
52
|
+
}
|
package/src/bootstrap.sh
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# A more robust way to get the script directory that works with symlinks
|
|
4
|
+
get_script_dir()
|
|
5
|
+
{
|
|
6
|
+
local SOURCE_PATH="${BASH_SOURCE[0]}"
|
|
7
|
+
local SYMLINK_DIR
|
|
8
|
+
local SCRIPT_DIR
|
|
9
|
+
# Resolve symlinks recursively
|
|
10
|
+
while [ -L "$SOURCE_PATH" ]; do
|
|
11
|
+
# Get symlink directory
|
|
12
|
+
SYMLINK_DIR="$( cd -P "$( dirname "$SOURCE_PATH" )" >/dev/null 2>&1 && pwd )"
|
|
13
|
+
# Resolve symlink target (relative or absolute)
|
|
14
|
+
SOURCE_PATH="$(readlink "$SOURCE_PATH")"
|
|
15
|
+
# Check if candidate path is relative or absolute
|
|
16
|
+
if [[ $SOURCE_PATH != /* ]]; then
|
|
17
|
+
# Candidate path is relative, resolve to full path
|
|
18
|
+
SOURCE_PATH=$SYMLINK_DIR/$SOURCE_PATH
|
|
19
|
+
fi
|
|
20
|
+
done
|
|
21
|
+
# Get final script directory path from fully resolved source path
|
|
22
|
+
SCRIPT_DIR="$(cd -P "$( dirname "$SOURCE_PATH" )" >/dev/null 2>&1 && pwd)"
|
|
23
|
+
echo "$SCRIPT_DIR"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
exec node --experimental-strip-types --disable-warning=ExperimentalWarning "$(get_script_dir)/index.ts" "$@"
|
package/src/commander.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Command } from "commander"
|
|
2
|
+
|
|
3
|
+
import { loadConfig } from "#config/config.ts"
|
|
4
|
+
import { Logger } from "#console/logger.ts"
|
|
5
|
+
import { withErrorHandler } from "#errors/withErrorHandler.ts"
|
|
6
|
+
import { loginToPlaycart } from "#modules/login.ts"
|
|
7
|
+
import { removeLoginCredentials } from "#modules/logout.ts"
|
|
8
|
+
import { buildSearchTemplate } from "#modules/search-templates/build.ts"
|
|
9
|
+
import { searchTemplateDevMode } from "#modules/search-templates/dev.ts"
|
|
10
|
+
import { pullSearchTemplate } from "#modules/search-templates/pull.ts"
|
|
11
|
+
import { pushSearchTemplate } from "#modules/search-templates/push.ts"
|
|
12
|
+
import { printSetupHelp } from "#modules/setup.ts"
|
|
13
|
+
import { printStatus } from "#modules/status.ts"
|
|
14
|
+
import { withSafeEnvironment } from "#utils/withSafeEnvironment.ts"
|
|
15
|
+
|
|
16
|
+
export async function runCLI(argv: string[]) {
|
|
17
|
+
const program = new Command()
|
|
18
|
+
program.name("nostocli").version("1.0.0").description("Nosto CLI tool. Use `nostocli setup` to get started.")
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command("login")
|
|
22
|
+
.description("Login with your Nosto account")
|
|
23
|
+
.option("--verbose", "set log level to debug")
|
|
24
|
+
.action(async options => {
|
|
25
|
+
await loadConfig({ options, allowIncomplete: true, projectPath: "." })
|
|
26
|
+
await withErrorHandler(async () => {
|
|
27
|
+
await loginToPlaycart()
|
|
28
|
+
})
|
|
29
|
+
Logger.info("Login successful")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command("logout")
|
|
34
|
+
.description("Delete stored login credentials")
|
|
35
|
+
.option("--verbose", "set log level to debug")
|
|
36
|
+
.action(() => {
|
|
37
|
+
removeLoginCredentials()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command("setup [projectPath]")
|
|
42
|
+
.description("Prints setup information and creates a configuration file")
|
|
43
|
+
.option("-m, --merchant <merchant>", "merchant to create config for")
|
|
44
|
+
.action(async (projectPath = ".", options) => {
|
|
45
|
+
await loadConfig({ options, allowIncomplete: true, projectPath: "." })
|
|
46
|
+
await withErrorHandler(() => printSetupHelp(projectPath, options))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.command("status [projectPath]")
|
|
51
|
+
.description("Print the configuration status")
|
|
52
|
+
.action(async (projectPath = ".", options) => {
|
|
53
|
+
await loadConfig({ options, allowIncomplete: true, projectPath: "." })
|
|
54
|
+
await withErrorHandler(() => printStatus(projectPath))
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const searchTemplates = program
|
|
58
|
+
.command("st")
|
|
59
|
+
.alias("search-templates")
|
|
60
|
+
.description("Search templates management commands")
|
|
61
|
+
|
|
62
|
+
searchTemplates
|
|
63
|
+
.command("pull [projectPath]")
|
|
64
|
+
.description("Pull the search-templates source from the Nosto VSCode Web")
|
|
65
|
+
.option("-p, --paths <files...>", "specific file paths to fetch (space-separated list)")
|
|
66
|
+
.option("--dry-run", "perform a dry run without making changes")
|
|
67
|
+
.option("--verbose", "set log level to debug")
|
|
68
|
+
.option("-f --force", "skip checking state, pull all files")
|
|
69
|
+
.action(async (projectPath = ".", options) => {
|
|
70
|
+
await withSafeEnvironment({ projectPath, options, skipSanityCheck: true }, async () => {
|
|
71
|
+
await pullSearchTemplate({
|
|
72
|
+
paths: options.paths ?? [],
|
|
73
|
+
force: options.force ?? false
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
searchTemplates
|
|
79
|
+
.command("push [projectPath]")
|
|
80
|
+
.description("Push the search-templates source to the VSCode Web")
|
|
81
|
+
.option("-p, --paths <files...>", "specific file paths to deploy (space-separated list)")
|
|
82
|
+
.option("--dry-run", "perform a dry run without making changes")
|
|
83
|
+
.option("--verbose", "set log level to debug")
|
|
84
|
+
.option("-f --force", "skip checking state, push all files")
|
|
85
|
+
.action(async (projectPath = ".", options) => {
|
|
86
|
+
await withSafeEnvironment({ projectPath, options }, async () => {
|
|
87
|
+
await buildSearchTemplate({ watch: false })
|
|
88
|
+
await pushSearchTemplate({
|
|
89
|
+
paths: options.paths ?? [],
|
|
90
|
+
force: options.force ?? false
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
searchTemplates
|
|
96
|
+
.command("build [projectPath]")
|
|
97
|
+
.description("Build the search-templates locally")
|
|
98
|
+
.option("--dry-run", "perform a dry run without making changes")
|
|
99
|
+
.option("--verbose", "set log level to debug")
|
|
100
|
+
.option("-w, --watch", "watch for changes and rebuild")
|
|
101
|
+
.action(async (projectPath = ".", options) => {
|
|
102
|
+
await withSafeEnvironment({ projectPath, options }, async () => {
|
|
103
|
+
await buildSearchTemplate({ watch: options.watch ?? false })
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
searchTemplates
|
|
108
|
+
.command("dev [projectPath]")
|
|
109
|
+
.description("Build the search-templates locally, watch for changes and continuously upload")
|
|
110
|
+
.option("--dry-run", "perform a dry run without making changes")
|
|
111
|
+
.option("--verbose", "set log level to debug")
|
|
112
|
+
.action(async (projectPath = ".", options) => {
|
|
113
|
+
await withSafeEnvironment({ projectPath, options }, async () => {
|
|
114
|
+
await searchTemplateDevMode()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await program.parseAsync(argv)
|
|
119
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import os from "os"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import z from "zod"
|
|
5
|
+
|
|
6
|
+
import { MissingConfigurationError } from "#errors/MissingConfigurationError.ts"
|
|
7
|
+
|
|
8
|
+
import { type AuthConfig, AuthConfigSchema } from "./schema.ts"
|
|
9
|
+
|
|
10
|
+
export const AuthConfigFilePath = path.join(os.homedir(), ".nosto", ".auth.json")
|
|
11
|
+
|
|
12
|
+
export function authFileExists() {
|
|
13
|
+
return fs.existsSync(AuthConfigFilePath)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getAuthFileMissingError() {
|
|
17
|
+
return new MissingConfigurationError(
|
|
18
|
+
`Auth file not found at: ${AuthConfigFilePath}. You will not be able to use most commands without running 'nosto login' first.`
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseAuthFile({ allowIncomplete }: { allowIncomplete?: boolean }): AuthConfig {
|
|
23
|
+
if (!allowIncomplete && !authFileExists()) {
|
|
24
|
+
throw getAuthFileMissingError()
|
|
25
|
+
} else if (!authFileExists()) {
|
|
26
|
+
return { user: "", token: "", expiresAt: new Date(0) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const configContent = fs.readFileSync(AuthConfigFilePath, "utf-8")
|
|
31
|
+
const rawConfig = JSON.parse(configContent)
|
|
32
|
+
return AuthConfigSchema.parse(rawConfig)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof z.ZodError) {
|
|
35
|
+
throw new Error(`Invalid auth file at ${AuthConfigFilePath}: ${error.message}`)
|
|
36
|
+
}
|
|
37
|
+
if (error instanceof SyntaxError) {
|
|
38
|
+
throw new Error(`Invalid JSON in auth file at ${AuthConfigFilePath}: ${error.message}`)
|
|
39
|
+
}
|
|
40
|
+
throw error
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { resolve } from "path"
|
|
2
|
+
|
|
3
|
+
import { cleanUrl } from "#api/utils.ts"
|
|
4
|
+
import { Logger } from "#console/logger.ts"
|
|
5
|
+
import { MissingConfigurationError } from "#errors/MissingConfigurationError.ts"
|
|
6
|
+
|
|
7
|
+
import { authFileExists, getAuthFileMissingError, parseAuthFile } from "./authConfig.ts"
|
|
8
|
+
import { getEnvConfig } from "./envConfig.ts"
|
|
9
|
+
import { parseConfigFile } from "./fileConfig.ts"
|
|
10
|
+
import {
|
|
11
|
+
AuthConfigSchema,
|
|
12
|
+
type Config,
|
|
13
|
+
PersistentConfigSchema,
|
|
14
|
+
RuntimeConfigSchema,
|
|
15
|
+
SearchTemplatesConfigSchema
|
|
16
|
+
} from "./schema.ts"
|
|
17
|
+
import { parseSearchTemplatesConfigFile } from "./searchTemplatesConfig.ts"
|
|
18
|
+
|
|
19
|
+
let isConfigLoaded = false
|
|
20
|
+
let cachedConfig: Config = {
|
|
21
|
+
...getDefaultConfig(),
|
|
22
|
+
auth: AuthConfigSchema.parse({
|
|
23
|
+
user: "",
|
|
24
|
+
token: "",
|
|
25
|
+
expiresAt: new Date(0)
|
|
26
|
+
}),
|
|
27
|
+
searchTemplates: {
|
|
28
|
+
mode: "unknown",
|
|
29
|
+
data: SearchTemplatesConfigSchema.parse({})
|
|
30
|
+
},
|
|
31
|
+
...RuntimeConfigSchema.parse({})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type LoadConfigProps = {
|
|
35
|
+
projectPath: string
|
|
36
|
+
options: object
|
|
37
|
+
allowIncomplete?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function loadConfig({ projectPath, options, allowIncomplete }: LoadConfigProps) {
|
|
41
|
+
const { dryRun, verbose } = RuntimeConfigSchema.parse({ ...options, projectPath })
|
|
42
|
+
|
|
43
|
+
if (isConfigLoaded) {
|
|
44
|
+
Logger.debug(`Using cached configuration`)
|
|
45
|
+
return cachedConfig
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const fullPath = resolve(projectPath)
|
|
49
|
+
Logger.debug(`Loading configuration from folder: ${fullPath}`)
|
|
50
|
+
if (!allowIncomplete && !authFileExists()) {
|
|
51
|
+
throw getAuthFileMissingError()
|
|
52
|
+
}
|
|
53
|
+
const authConfig = parseAuthFile({ allowIncomplete })
|
|
54
|
+
const searchTemplatesConfig = await parseSearchTemplatesConfigFile({ projectPath })
|
|
55
|
+
const fileConfig = parseConfigFile({ projectPath, allowIncomplete })
|
|
56
|
+
const envConfig = getEnvConfig()
|
|
57
|
+
|
|
58
|
+
const baseConfig = allowIncomplete ? getDefaultConfig() : {}
|
|
59
|
+
|
|
60
|
+
const combinedConfig = {
|
|
61
|
+
...baseConfig,
|
|
62
|
+
...fileConfig,
|
|
63
|
+
...envConfig
|
|
64
|
+
}
|
|
65
|
+
if (!combinedConfig.merchant && !allowIncomplete) {
|
|
66
|
+
throw new MissingConfigurationError("Invalid configuration: Missing merchant ID")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const persistentConfig = PersistentConfigSchema.parse(combinedConfig)
|
|
71
|
+
cachedConfig = {
|
|
72
|
+
...persistentConfig,
|
|
73
|
+
auth: authConfig,
|
|
74
|
+
searchTemplates: searchTemplatesConfig,
|
|
75
|
+
apiUrl: cleanUrl(persistentConfig.apiUrl),
|
|
76
|
+
libraryUrl: cleanUrl(persistentConfig.libraryUrl),
|
|
77
|
+
logLevel: verbose ? "debug" : persistentConfig.logLevel,
|
|
78
|
+
projectPath,
|
|
79
|
+
dryRun,
|
|
80
|
+
verbose
|
|
81
|
+
}
|
|
82
|
+
updateLoggerContext(cachedConfig)
|
|
83
|
+
isConfigLoaded = true
|
|
84
|
+
return cachedConfig
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error("Failed to load configuration", { cause: error })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function updateLoggerContext(config: Config) {
|
|
91
|
+
Logger.context = {
|
|
92
|
+
logLevel: config.logLevel,
|
|
93
|
+
merchantId: config.merchant,
|
|
94
|
+
isDryRun: config.dryRun
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getCachedConfig() {
|
|
99
|
+
return cachedConfig
|
|
100
|
+
}
|
|
101
|
+
export function getCachedSearchTemplatesConfig() {
|
|
102
|
+
return cachedConfig.searchTemplates.data
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function getDefaultConfig() {
|
|
106
|
+
return PersistentConfigSchema.parse({
|
|
107
|
+
merchant: ""
|
|
108
|
+
})
|
|
109
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type EnvironmentConfig, EnvironmentConfigSchema } from "./schema.ts"
|
|
2
|
+
|
|
3
|
+
export const EnvVariables = {
|
|
4
|
+
apiKey: "NOSTO_API_KEY",
|
|
5
|
+
merchant: "NOSTO_MERCHANT",
|
|
6
|
+
templatesEnv: "NOSTO_TEMPLATES_ENV",
|
|
7
|
+
apiUrl: "NOSTO_API_URL",
|
|
8
|
+
libraryUrl: "NOSTO_LIBRARY_URL",
|
|
9
|
+
logLevel: "NOSTO_LOG_LEVEL",
|
|
10
|
+
maxRequests: "NOSTO_MAX_REQUESTS"
|
|
11
|
+
} satisfies Record<keyof EnvironmentConfig, string>
|
|
12
|
+
|
|
13
|
+
export function getEnvConfig(): EnvironmentConfig {
|
|
14
|
+
const config = Object.entries(EnvVariables).reduce<Record<string, string>>((acc, [key, envVar]) => {
|
|
15
|
+
const value = process.env[envVar]
|
|
16
|
+
if (value) {
|
|
17
|
+
acc[key] = value
|
|
18
|
+
}
|
|
19
|
+
return acc
|
|
20
|
+
}, {})
|
|
21
|
+
|
|
22
|
+
return EnvironmentConfigSchema.parse(config)
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import z from "zod"
|
|
4
|
+
|
|
5
|
+
import { Logger } from "#console/logger.ts"
|
|
6
|
+
|
|
7
|
+
import { type PartialPersistentConfig, PartialPersistentConfigSchema } from "./schema.ts"
|
|
8
|
+
|
|
9
|
+
export function parseConfigFile({
|
|
10
|
+
projectPath,
|
|
11
|
+
allowIncomplete
|
|
12
|
+
}: {
|
|
13
|
+
projectPath: string
|
|
14
|
+
allowIncomplete?: boolean
|
|
15
|
+
}): PartialPersistentConfig {
|
|
16
|
+
const configPath = path.join(projectPath, ".nosto.json")
|
|
17
|
+
|
|
18
|
+
const configFileMissing = !fs.existsSync(configPath)
|
|
19
|
+
if (allowIncomplete && configFileMissing) {
|
|
20
|
+
return {}
|
|
21
|
+
} else if (configFileMissing) {
|
|
22
|
+
Logger.warn(`Configuration file not found at: ${configPath}. Will try to use environment variables.`)
|
|
23
|
+
return {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const configContent = fs.readFileSync(configPath, "utf-8")
|
|
28
|
+
const rawConfig = JSON.parse(configContent)
|
|
29
|
+
return PartialPersistentConfigSchema.parse(rawConfig)
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error instanceof z.ZodError) {
|
|
32
|
+
throw new Error(`Invalid configuration file at ${configPath}: ${error.message}`)
|
|
33
|
+
}
|
|
34
|
+
if (error instanceof SyntaxError) {
|
|
35
|
+
throw new Error(`Invalid JSON in configuration file at ${configPath}: ${error.message}`)
|
|
36
|
+
}
|
|
37
|
+
throw error
|
|
38
|
+
}
|
|
39
|
+
}
|