@mboucher/ffe-setup 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +118 -2
- package/bin/run.js +27 -0
- package/package.json +25 -4
- package/src/install-skill.js +86 -0
- package/src/setup.js +89 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcel Boucher
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
#
|
|
1
|
+
# FFE Setup
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Explicit installer for the FFE CLI and portable template validation skill.
|
|
4
|
+
Requires Node.js 22.13+, npm, and Azure CLI (`az`) on PATH for full setup.
|
|
5
|
+
MCP installation/registration, Azure CLI installation, and permission grants
|
|
6
|
+
remain separate.
|
|
7
|
+
|
|
8
|
+
## Set Up FFE
|
|
9
|
+
|
|
10
|
+
After the skill and setup packages are published to npm, run:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx @mboucher/ffe-setup --client vscode
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Setup checks Node.js, npm, and `az version` before installing anything. Missing
|
|
17
|
+
or broken prerequisites stop setup with instructions. Install Azure CLI from
|
|
18
|
+
https://aka.ms/installazurecli if needed. Availability checks do not log in,
|
|
19
|
+
query resources, or verify Azure permissions.
|
|
20
|
+
|
|
21
|
+
It then runs `npm install --global @mboucher/ffe-cli@latest`, verifies the installed
|
|
22
|
+
global executable with `--version`, and copies the skill. Re-running full setup
|
|
23
|
+
can update an existing global FFE CLI. Ensure npm's global executable directory
|
|
24
|
+
(printed by setup) is on PATH. Installation uses the current npm prefix, does not
|
|
25
|
+
request administrator privileges, and does not retry failed installations with
|
|
26
|
+
elevated privileges. Fix prefix write access or registry connectivity before retrying.
|
|
27
|
+
If a later step fails, already completed installations are retained.
|
|
28
|
+
|
|
29
|
+
In an interactive terminal, setup asks `Run "ffe auth login" now? [y/N]`.
|
|
30
|
+
Only an explicit yes starts the CLI's secure Adobe credential flow, with terminal
|
|
31
|
+
input/output inherited directly. Setup does not collect or store credentials.
|
|
32
|
+
Declining, passing `--no-login`, or running without a TTY prints `ffe auth login`
|
|
33
|
+
for later instead. A failed or cancelled login exits unsuccessfully but leaves
|
|
34
|
+
the installation in place. Azure storage authentication is separate: run
|
|
35
|
+
`az login` before storage commands. No Azure login runs automatically.
|
|
36
|
+
|
|
37
|
+
For automated installs, skip the login prompt explicitly:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx @mboucher/ffe-setup --client vscode --no-login
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
To install only the portable skill without checking Azure CLI or installing FFE:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npx @mboucher/ffe-setup --client vscode --skill-only
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The default is workspace scope in the current directory. Choose a different
|
|
50
|
+
project or install for your user explicitly:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npx @mboucher/ffe-setup --client vscode --workspace /path/to/project
|
|
54
|
+
npx @mboucher/ffe-setup --client vscode --scope user
|
|
55
|
+
npx @mboucher/ffe-setup --client claude-code --workspace /path/to/project
|
|
56
|
+
npx @mboucher/ffe-setup --client claude-code --scope user
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Client | Workspace Skill Directory | User Skill Directory |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `vscode` | `.github/skills/ffe-template-validation-skill` | `~/.copilot/skills/ffe-template-validation-skill` |
|
|
62
|
+
| `claude-code` | `.claude/skills/ffe-template-validation-skill` | `~/.claude/skills/ffe-template-validation-skill` |
|
|
63
|
+
|
|
64
|
+
For any other assistant, choose its documented skill search directory directly:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
npx @mboucher/ffe-setup --skills-dir /path/to/skills
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Do not combine `--skills-dir` with client or scope options. The skill is placed
|
|
71
|
+
under a `ffe-template-validation-skill` subdirectory of the selected directory.
|
|
72
|
+
Reload skill discovery in your assistant after installation. No skill is
|
|
73
|
+
installed merely by adding this npm package as a dependency; installation is an
|
|
74
|
+
explicit command, not a `postinstall` side effect.
|
|
75
|
+
|
|
76
|
+
## Existing Installations
|
|
77
|
+
|
|
78
|
+
The installer copies the skill entry point, references, README, and license.
|
|
79
|
+
Identical skill installations are left unchanged. If any installed file differs, or
|
|
80
|
+
the target is a file or symbolic link, installation stops without overwriting
|
|
81
|
+
it. Back up or move an existing installation before intentionally replacing it.
|
|
82
|
+
No force-overwrite or automatic uninstall behavior is provided.
|
|
83
|
+
|
|
84
|
+
## Local Development
|
|
85
|
+
|
|
86
|
+
Before publishing the skill, keep its checkout beside this repository:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npm run dev:link
|
|
90
|
+
npm test
|
|
91
|
+
node bin/run.js --help
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`dev:link` links the sibling `ffe-template-validation-skill` package without
|
|
95
|
+
changing dependency specifications. To try the installer locally, use
|
|
96
|
+
`node bin/run.js --client vscode --workspace /path/to/project`.
|
|
97
|
+
|
|
98
|
+
Tests install into temporary directories, not your personal skills directory.
|
|
99
|
+
They cover metadata discovery, destination selection, complete copies,
|
|
100
|
+
repeatability, conflict protection, and packed npm artifact installation. Full
|
|
101
|
+
setup tests use injected commands or temporary fake executables; they never
|
|
102
|
+
install global packages or start real authentication.
|
|
103
|
+
|
|
104
|
+
## Release
|
|
105
|
+
|
|
106
|
+
Publish `@mboucher/ffe-template-validation-skill` 0.1.0 first. Then, from this
|
|
107
|
+
repository:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
npm install
|
|
111
|
+
npm test
|
|
112
|
+
npm run deploy -- --dry-run
|
|
113
|
+
npm run deploy
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`npm install` resolves the published skill and generates the registry-backed
|
|
117
|
+
lockfile; commit it before release. Use `npm ci` for subsequent clean installs.
|
|
118
|
+
Publishing runs tests first. Authenticate to npm through its trusted local flow;
|
|
119
|
+
never place tokens in scripts. No publication is performed by tests.
|
package/bin/run.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {parseArgs} from 'node:util'
|
|
3
|
+
import {skillsDirectory} from '../src/install-skill.js'
|
|
4
|
+
import {setup} from '../src/setup.js'
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
const {values} = parseArgs({options: {
|
|
8
|
+
client: {type: 'string'},
|
|
9
|
+
scope: {type: 'string'},
|
|
10
|
+
workspace: {type: 'string'},
|
|
11
|
+
'skills-dir': {type: 'string'},
|
|
12
|
+
'skill-only': {type: 'boolean'},
|
|
13
|
+
'no-login': {type: 'boolean'},
|
|
14
|
+
help: {type: 'boolean', short: 'h'},
|
|
15
|
+
}, allowPositionals: false})
|
|
16
|
+
if (values.help) {
|
|
17
|
+
console.log('Usage: ffe-setup --client vscode|claude-code [--scope workspace|user] [--workspace PATH]\n ffe-setup --skills-dir PATH\n\nChecks Node.js 22.13+, npm, and Azure CLI, installs FFE CLI globally and the\nvalidation skill, then offers to run "ffe auth login" in interactive terminals.\nWorkspace scope is the default. Does not install Azure CLI or register MCP.\n\nOptions:\n --skill-only Install only the skill; skip external tools and authentication.\n --no-login Install normally without prompting for Adobe login.')
|
|
18
|
+
} else {
|
|
19
|
+
const directory = skillsDirectory({
|
|
20
|
+
client: values.client, scope: values.scope, workspace: values.workspace, skillsDir: values['skills-dir'],
|
|
21
|
+
})
|
|
22
|
+
await setup({directory, skillOnly: values['skill-only'], noLogin: values['no-login']})
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(error instanceof Error ? error.message : 'FFE setup failed.')
|
|
26
|
+
process.exitCode = 1
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
"name": "@mboucher/ffe-setup",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Dependency validation, FFE CLI installation, and portable skill setup",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ffe-setup": "bin/run.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22.13.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test test/*.test.js",
|
|
19
|
+
"deploy": "npm publish --access public",
|
|
20
|
+
"dev:link": "npm install --no-save --package-lock=false ../ffe-template-validation-skill",
|
|
21
|
+
"prepublishOnly": "npm test"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@mboucher/ffe-template-validation-skill": "^0.1.0",
|
|
25
|
+
"cross-spawn": "^7.0.6"
|
|
26
|
+
}
|
|
6
27
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {lstat, mkdir, readFile, readdir, rm, writeFile} from 'node:fs/promises'
|
|
2
|
+
import {homedir} from 'node:os'
|
|
3
|
+
import {dirname, join, relative, resolve} from 'node:path'
|
|
4
|
+
import {fileURLToPath} from 'node:url'
|
|
5
|
+
|
|
6
|
+
export const SKILL_NAME = 'ffe-template-validation-skill'
|
|
7
|
+
|
|
8
|
+
export function skillsDirectory({client, scope, workspace, skillsDir, home = homedir()}) {
|
|
9
|
+
if (skillsDir !== undefined) {
|
|
10
|
+
if (!skillsDir.trim() || client !== undefined || scope !== undefined || workspace !== undefined) {
|
|
11
|
+
throw new Error('Use --skills-dir PATH alone, without --client, --scope, or --workspace.')
|
|
12
|
+
}
|
|
13
|
+
return resolve(skillsDir)
|
|
14
|
+
}
|
|
15
|
+
if (!['vscode', 'claude-code'].includes(client)) {
|
|
16
|
+
throw new Error('Select --client vscode or --client claude-code, or supply --skills-dir PATH.')
|
|
17
|
+
}
|
|
18
|
+
scope ??= 'workspace'
|
|
19
|
+
if (!['workspace', 'user'].includes(scope)) throw new Error('--scope must be workspace or user.')
|
|
20
|
+
if (scope === 'user' && workspace !== undefined) throw new Error('--workspace cannot be combined with --scope user.')
|
|
21
|
+
if (workspace !== undefined && !workspace.trim()) throw new Error('--workspace must be a nonempty path.')
|
|
22
|
+
const root = scope === 'user' ? home : resolve(workspace ?? process.cwd())
|
|
23
|
+
const directory = client === 'claude-code' ? '.claude' : scope === 'user' ? '.copilot' : '.github'
|
|
24
|
+
return join(root, directory, 'skills')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function addTree(directory, root, files) {
|
|
28
|
+
for (const entry of await readdir(directory, {withFileTypes: true})) {
|
|
29
|
+
const path = join(directory, entry.name)
|
|
30
|
+
if (entry.isSymbolicLink()) throw new Error(`Refusing a symbolic link inside the skill: ${entry.name}`)
|
|
31
|
+
if (entry.isDirectory()) await addTree(path, root, files)
|
|
32
|
+
else if (entry.isFile()) files.set(relative(root, path), await readFile(path))
|
|
33
|
+
else throw new Error(`Unsupported skill entry: ${entry.name}`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function bundle(source) {
|
|
38
|
+
const files = new Map()
|
|
39
|
+
for (const name of ['SKILL.md', 'README.md', 'LICENSE', 'references']) {
|
|
40
|
+
const path = join(source, name)
|
|
41
|
+
const entry = await lstat(path)
|
|
42
|
+
if (entry.isSymbolicLink()) throw new Error(`Refusing a symbolic link inside the skill: ${name}`)
|
|
43
|
+
if (name === 'references' && entry.isDirectory()) await addTree(path, source, files)
|
|
44
|
+
else if (name !== 'references' && entry.isFile()) files.set(name, await readFile(path))
|
|
45
|
+
else throw new Error(`Invalid skill package entry: ${name}`)
|
|
46
|
+
}
|
|
47
|
+
for (const name of ['references/checklist.md', 'references/ffe-cli.md']) {
|
|
48
|
+
if (!files.has(name)) throw new Error(`The skill package is missing ${name}.`)
|
|
49
|
+
}
|
|
50
|
+
return files
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function installSkill({directory, source}) {
|
|
54
|
+
source ??= dirname(fileURLToPath(import.meta.resolve('@mboucher/ffe-template-validation-skill/package.json')))
|
|
55
|
+
const files = await bundle(source)
|
|
56
|
+
const target = join(resolve(directory), SKILL_NAME)
|
|
57
|
+
let existing
|
|
58
|
+
try {
|
|
59
|
+
existing = await lstat(target)
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error.code !== 'ENOENT') throw error
|
|
62
|
+
}
|
|
63
|
+
if (existing) {
|
|
64
|
+
if (!existing.isDirectory() || existing.isSymbolicLink()) throw new Error(`Refusing to replace ${target}.`)
|
|
65
|
+
const installed = new Map()
|
|
66
|
+
await addTree(target, target, installed)
|
|
67
|
+
if (files.size === installed.size && [...files].every(([name, data]) => installed.get(name)?.equals(data))) {
|
|
68
|
+
return {target, status: 'unchanged'}
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Existing skill differs at ${target}. Back it up or move it before installing; no files were changed.`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await mkdir(dirname(target), {recursive: true})
|
|
74
|
+
await mkdir(target)
|
|
75
|
+
try {
|
|
76
|
+
for (const [name, data] of files) {
|
|
77
|
+
const path = join(target, name)
|
|
78
|
+
await mkdir(dirname(path), {recursive: true})
|
|
79
|
+
await writeFile(path, data, {flag: 'wx'})
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
await rm(target, {recursive: true, force: true})
|
|
83
|
+
throw error
|
|
84
|
+
}
|
|
85
|
+
return {target, status: 'installed'}
|
|
86
|
+
}
|
package/src/setup.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import spawn from 'cross-spawn'
|
|
2
|
+
import {join, dirname} from 'node:path'
|
|
3
|
+
import {createInterface} from 'node:readline/promises'
|
|
4
|
+
import {installSkill} from './install-skill.js'
|
|
5
|
+
|
|
6
|
+
export function runCommand(command, args, {inherit = false, timeout = 30_000} = {}) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn(command, args, {
|
|
9
|
+
stdio: inherit ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
|
10
|
+
...(timeout ? {signal: AbortSignal.timeout(timeout)} : {}),
|
|
11
|
+
})
|
|
12
|
+
let stdout = ''
|
|
13
|
+
child.stdout?.on('data', (data) => { stdout += data })
|
|
14
|
+
child.stderr?.resume()
|
|
15
|
+
child.on('error', reject)
|
|
16
|
+
child.on('close', (code, signal) => {
|
|
17
|
+
if (code === 0) resolve(stdout.trim())
|
|
18
|
+
else reject(new Error(`${command} ${args.join(' ')} failed (${signal ?? `exit ${code}`}).`))
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function confirmLogin() {
|
|
24
|
+
const prompt = createInterface({input: process.stdin, output: process.stdout})
|
|
25
|
+
try {
|
|
26
|
+
const answer = await prompt.question('Run "ffe auth login" now? [y/N] ')
|
|
27
|
+
return /^(y|yes)$/i.test(answer.trim())
|
|
28
|
+
} finally {
|
|
29
|
+
prompt.close()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function setup({directory, skillOnly = false, noLogin = false}, {
|
|
34
|
+
run = runCommand, install = installSkill, confirm = confirmLogin,
|
|
35
|
+
interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
36
|
+
nodeVersion = process.versions.node, platform = process.platform, log = console.log,
|
|
37
|
+
} = {}) {
|
|
38
|
+
const [major, minor] = nodeVersion.split('.').map(Number)
|
|
39
|
+
if (!(major > 22 || (major === 22 && minor >= 13))) {
|
|
40
|
+
throw new Error(`Node.js 22.13+ is required; found ${nodeVersion}. Upgrade Node.js and rerun setup.`)
|
|
41
|
+
}
|
|
42
|
+
let executable
|
|
43
|
+
if (!skillOnly) {
|
|
44
|
+
log(`Node.js ${nodeVersion}: OK`)
|
|
45
|
+
try {
|
|
46
|
+
const version = await run('npm', ['--version'])
|
|
47
|
+
log(`npm ${version}: OK`)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error('npm is unavailable. Install npm with Node.js and ensure it is on PATH, then rerun setup.', {cause: error})
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const version = JSON.parse(await run('az', ['version', '--output', 'json']))['azure-cli']
|
|
53
|
+
if (typeof version !== 'string' || !version) throw new Error('Invalid Azure CLI version response.')
|
|
54
|
+
log(`Azure CLI ${version}: OK`)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error('Azure CLI is unavailable or failed validation. Install or repair it from https://aka.ms/installazurecli and ensure "az" is on PATH, then rerun setup. Use --skill-only for offline skill installation.', {cause: error})
|
|
57
|
+
}
|
|
58
|
+
log('Installing FFE CLI globally: npm install --global @mboucher/ffe-cli@latest')
|
|
59
|
+
try {
|
|
60
|
+
await run('npm', ['install', '--global', '@mboucher/ffe-cli@latest'], {inherit: true, timeout: 0})
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new Error('FFE CLI installation failed. Check npm output, registry access, and write access to your npm global prefix. Setup does not elevate privileges. Fix the issue and rerun setup.', {cause: error})
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const prefix = await run('npm', ['prefix', '--global'])
|
|
66
|
+
if (!prefix) throw new Error('npm returned an empty global prefix.')
|
|
67
|
+
executable = platform === 'win32' ? join(prefix, 'ffe.cmd') : join(prefix, 'bin', 'ffe')
|
|
68
|
+
log(`FFE CLI: ${await run(executable, ['--version'])}`)
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new Error('FFE CLI was installed but verification failed. Check "ffe --version" and your Node.js installation before rerunning setup.', {cause: error})
|
|
71
|
+
}
|
|
72
|
+
log(`Ensure the global executable directory is on PATH: ${dirname(executable)}`)
|
|
73
|
+
}
|
|
74
|
+
const result = await install({directory})
|
|
75
|
+
log(`${result.status === 'installed' ? 'Installed' : 'Already installed'}: ${result.target}`)
|
|
76
|
+
log('Reload skill discovery in your assistant. MCP registration is separate.')
|
|
77
|
+
if (skillOnly) return result
|
|
78
|
+
log('Azure storage authentication is separate: run "az login" before storage commands.')
|
|
79
|
+
if (noLogin || !interactive || !await confirm()) {
|
|
80
|
+
log('To authenticate with Adobe later, run: ffe auth login')
|
|
81
|
+
return result
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
await run(executable, ['auth', 'login'], {inherit: true, timeout: 0})
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error('Installation completed, but Adobe login failed or was cancelled. Retry in an interactive terminal with "ffe auth login".', {cause: error})
|
|
87
|
+
}
|
|
88
|
+
return result
|
|
89
|
+
}
|