@fraylabs/possible 0.2.0 → 0.3.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/package.json +4 -9
- package/src/directory.mjs +37 -0
- package/src/index.mjs +22 -12
- package/assets/possible/SKILL.md +0 -117
- package/assets/possible/agents/openai.yaml +0 -4
- package/src/init.mjs +0 -150
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fraylabs/possible",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Create, publish, discover, and reuse source-owned AI Outcomes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -23,15 +23,10 @@
|
|
|
23
23
|
"bin": {
|
|
24
24
|
"possible": "src/index.mjs"
|
|
25
25
|
},
|
|
26
|
-
"files": [
|
|
27
|
-
"assets/possible",
|
|
28
|
-
"src"
|
|
29
|
-
],
|
|
26
|
+
"files": ["src"],
|
|
30
27
|
"scripts": {
|
|
31
|
-
"build": "node
|
|
32
|
-
"test": "node --test test/*.test.mjs"
|
|
33
|
-
"sync:skill": "node scripts/sync-skill.mjs",
|
|
34
|
-
"prepack": "node scripts/sync-skill.mjs"
|
|
28
|
+
"build": "node --check src/index.mjs && node --check src/directory.mjs",
|
|
29
|
+
"test": "node --test test/*.test.mjs"
|
|
35
30
|
},
|
|
36
31
|
"engines": {
|
|
37
32
|
"node": ">=22"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const DEFAULT_ENDPOINT = "https://abutwsaahahtbtlopczi.supabase.co/functions/v1/outcome-directory";
|
|
2
|
+
|
|
3
|
+
const directoryEndpoint = () => process.env.POSSIBLE_DIRECTORY_ENDPOINT?.trim() || DEFAULT_ENDPOINT;
|
|
4
|
+
|
|
5
|
+
async function requestDirectory(parameters, fetchImplementation = fetch) {
|
|
6
|
+
const url = new URL(directoryEndpoint());
|
|
7
|
+
for (const [name, value] of Object.entries(parameters)) url.searchParams.set(name, value);
|
|
8
|
+
const response = await fetchImplementation(url, { headers: { accept: "application/json" } });
|
|
9
|
+
const body = await response.json().catch(() => ({}));
|
|
10
|
+
if (!response.ok) throw new Error(body.error || `Possible directory returned HTTP ${response.status}`);
|
|
11
|
+
return body;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function searchOutcomes(query, options = {}) {
|
|
15
|
+
const normalized = String(query ?? "").trim();
|
|
16
|
+
if (!normalized) throw new Error("Search requires an ordinary-language query");
|
|
17
|
+
const body = await requestDirectory({ q: normalized, limit: "5" }, options.fetchImplementation);
|
|
18
|
+
return Array.isArray(body.outcomes) ? body.outcomes : [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function fetchOutcome(id, options = {}) {
|
|
22
|
+
const normalized = String(id ?? "").trim();
|
|
23
|
+
if (!normalized) throw new Error("Fetch requires an Outcome ID from search results");
|
|
24
|
+
const body = await requestDirectory({ id: normalized }, options.fetchImplementation);
|
|
25
|
+
if (!body.outcome || typeof body.outcome.prompt !== "string") throw new Error("Possible returned an invalid Outcome");
|
|
26
|
+
return body.outcome;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatSearchResults(outcomes) {
|
|
30
|
+
if (!outcomes.length) return "No matching Outcomes found.\n";
|
|
31
|
+
return `${outcomes.map((outcome, index) => [
|
|
32
|
+
`${index + 1}. ${outcome.title}`,
|
|
33
|
+
` ${outcome.summary}`,
|
|
34
|
+
` ID: ${outcome.id}`,
|
|
35
|
+
` Source: ${outcome.source_locator}`,
|
|
36
|
+
].join("\n")).join("\n\n")}\n`;
|
|
37
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -2,25 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { runBookmarkCommand } from "./bookmarks.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { fetchOutcome, formatSearchResults, searchOutcomes } from "./directory.mjs";
|
|
6
6
|
import { addOutcomeSource, createOutcome, publishOutcomeSource, useOutcome, validateOutcomes } from "./outcome-commands.mjs";
|
|
7
7
|
|
|
8
8
|
const HELP = `Possible CLI
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
11
|
-
possible init
|
|
12
11
|
possible create <slug>
|
|
13
12
|
possible validate [directory]
|
|
14
13
|
possible publish [owner/repository | https://publisher.example]
|
|
14
|
+
possible search <ordinary-language query>
|
|
15
|
+
possible fetch <outcome-id>
|
|
15
16
|
possible add <owner/repository | https://publisher.example>
|
|
16
17
|
possible use <source>@<slug>
|
|
17
18
|
possible bookmark <command>
|
|
18
19
|
|
|
19
20
|
Commands:
|
|
20
|
-
init Install the optional Possible prompt-preparation skill into this project
|
|
21
21
|
create Create outcome.json, outcome.md, prompt.md, and media/ for one Outcome
|
|
22
22
|
validate Validate every Outcome folder below a directory
|
|
23
23
|
publish Validate and submit one public publisher source; no account required
|
|
24
|
+
search Find relevant Outcomes in the live public directory
|
|
25
|
+
fetch Print one directory Outcome's exact prompt
|
|
24
26
|
add Discover a public source and save it to .possible/sources.json
|
|
25
27
|
use Print one exact execution prompt to standard output
|
|
26
28
|
bookmark add | list | remove locally saved Outcome slugs
|
|
@@ -38,6 +40,22 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
|
38
40
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
39
41
|
process.exitCode = 1;
|
|
40
42
|
}
|
|
43
|
+
} else if (args[0] === "search" && args.length >= 2) {
|
|
44
|
+
try {
|
|
45
|
+
const outcomes = await searchOutcomes(args.slice(1).join(" "));
|
|
46
|
+
process.stdout.write(formatSearchResults(outcomes));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
}
|
|
51
|
+
} else if (args[0] === "fetch" && args.length === 2) {
|
|
52
|
+
try {
|
|
53
|
+
const outcome = await fetchOutcome(args[1]);
|
|
54
|
+
process.stdout.write(`${outcome.prompt}\n`);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
}
|
|
41
59
|
} else if (args[0] === "create" && args.length === 2) {
|
|
42
60
|
try {
|
|
43
61
|
const folder = await createOutcome(args[1]);
|
|
@@ -81,15 +99,7 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
|
81
99
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
82
100
|
process.exitCode = 1;
|
|
83
101
|
}
|
|
84
|
-
} else
|
|
102
|
+
} else {
|
|
85
103
|
process.stderr.write(`Unknown command: ${args.join(" ")}\n\n${HELP}`);
|
|
86
104
|
process.exitCode = 1;
|
|
87
|
-
} else {
|
|
88
|
-
try {
|
|
89
|
-
const result = await installPossibleSkill();
|
|
90
|
-
process.stdout.write(`${result.changed ? "Possible installed" : "Possible is already installed"} at ${result.installPath}\n\nOpen Codex in this project and type:\n\n $possible\n`);
|
|
91
|
-
} catch (error) {
|
|
92
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
93
|
-
process.exitCode = 1;
|
|
94
|
-
}
|
|
95
105
|
}
|
package/assets/possible/SKILL.md
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: possible
|
|
3
|
-
description: Discover proven Outcomes and turn a rough request into one complete execution prompt for a fresh agent. Also author and validate source-owned Outcomes for Possible.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Possible
|
|
7
|
-
|
|
8
|
-
Possible connects three things:
|
|
9
|
-
|
|
10
|
-
- the user's **original prompt**;
|
|
11
|
-
- the complete **execution prompt** sent to a working agent;
|
|
12
|
-
- the resulting **Outcome**.
|
|
13
|
-
|
|
14
|
-
An Outcome is precedent to inspect and remix, not a rigid workflow. Preserve useful creative freedom while giving a fresh agent the context, current methods, and concrete inputs it needs.
|
|
15
|
-
|
|
16
|
-
## Discover before writing
|
|
17
|
-
|
|
18
|
-
Preserve the user's original prompt verbatim. Determine whether they want to browse possibilities, prepare a prompt, execute work, or publish completed work.
|
|
19
|
-
|
|
20
|
-
For browsing or execution, call `search_outcomes` using ordinary language. Show up to five relevant Outcomes when the user is browsing. When preparing work, fetch the strongest candidates with `fetch_outcome` and read their exact prompts. Search scores indicate text similarity, not quality.
|
|
21
|
-
|
|
22
|
-
Use Outcomes as concrete precedent. If none fit, use current primary sources rather than forcing an unrelated example. Prefer official documentation, current source repositories and registries, then reproducible community examples. Check volatile information at run time. Popularity is a discovery signal, not proof that an Outcome is good.
|
|
23
|
-
|
|
24
|
-
## Resolve consequential unknowns
|
|
25
|
-
|
|
26
|
-
Collect only what can materially improve execution:
|
|
27
|
-
|
|
28
|
-
- the intended result, audience, use, and strong preferences;
|
|
29
|
-
- supplied files, measurements, references, credentials, and constraints;
|
|
30
|
-
- required inputs that the executor cannot safely invent;
|
|
31
|
-
- relevant prior Outcomes and what they actually produced;
|
|
32
|
-
- current Products, Skills, tools, environment, permissions, and limits;
|
|
33
|
-
- what the user will inspect to decide that the result is finished.
|
|
34
|
-
|
|
35
|
-
Ask the fewest questions necessary. Discover safe facts yourself. Infer harmless aesthetic details when a restrained default is sufficient. Do not turn the conversation into a form.
|
|
36
|
-
|
|
37
|
-
## Prepare one execution prompt
|
|
38
|
-
|
|
39
|
-
Write one readable, self-contained prompt for a fresh agent. It should naturally state:
|
|
40
|
-
|
|
41
|
-
- the exact result and who it is for;
|
|
42
|
-
- relevant user context and supplied materials;
|
|
43
|
-
- concrete requirements and preferences;
|
|
44
|
-
- current Products, Skills, or tools that matter;
|
|
45
|
-
- deliverables and where to place them;
|
|
46
|
-
- constraints, permissions, and separately authorized external actions;
|
|
47
|
-
- what the user will inspect to judge the result;
|
|
48
|
-
- unknowns the executor must preserve rather than invent.
|
|
49
|
-
|
|
50
|
-
Adapt prior prompts to the current request, date, model, environment, and evidence. Never substitute a summary for a strong full prompt. Never claim an old method is current without checking. Keep the published prompt distinct from the new prompt you prepare.
|
|
51
|
-
|
|
52
|
-
Before handoff, confirm that a fresh agent can start without hidden conversation history, missing essential files, consequential unresolved choices, unsupported current claims, or unclear success conditions. Research further or ask one focused question if it cannot.
|
|
53
|
-
|
|
54
|
-
Show the proposed execution prompt and name the prior Outcomes and current official sources that materially shaped it.
|
|
55
|
-
|
|
56
|
-
## Hand off once
|
|
57
|
-
|
|
58
|
-
After approval, send the execution prompt unchanged to a fresh subagent when that capability is available. Include explicit paths or attachments for every supplied file. One-shot means complete starting context; it does not forbid the executor from inspecting, testing, or repairing its work.
|
|
59
|
-
|
|
60
|
-
If fresh subagents are unavailable, return the execution prompt in a copyable block. Do not pretend a handoff occurred.
|
|
61
|
-
|
|
62
|
-
Products and Skills describe capabilities. They do not grant permission to spend money, purchase, publish, deploy, contact people, fabricate, operate hardware, or perform another external action.
|
|
63
|
-
|
|
64
|
-
## Author a completed Outcome
|
|
65
|
-
|
|
66
|
-
When the user wants to publish completed work, inspect the real result and preserve the exact prompt and provenance. Create one folder:
|
|
67
|
-
|
|
68
|
-
```text
|
|
69
|
-
outcomes.json
|
|
70
|
-
outcomes/<slug>/
|
|
71
|
-
outcome.json
|
|
72
|
-
outcome.md
|
|
73
|
-
prompt.md
|
|
74
|
-
media/ optional
|
|
75
|
-
artifacts/ optional
|
|
76
|
-
inputs/ optional
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
`outcomes.json` is the repository-root publisher index and is created or updated by the CLI. `outcome.md` is the canonical human page: one H1 title, one clear opening summary, and useful formatted explanation. `prompt.md` is the exact reusable execution prompt. `outcome.json` contains machine metadata only: author, authored timestamp, models and agents, required inputs, Products, Skills with last-reviewed commits, and media or artifact references.
|
|
80
|
-
|
|
81
|
-
Do not reconstruct absent provenance as fact. Mark unknown values honestly. Products and models belong in `outcome.json`; deliverables and detailed work instructions belong in `prompt.md`; result explanation belongs in `outcome.md`.
|
|
82
|
-
|
|
83
|
-
Use the CLI to scaffold and validate:
|
|
84
|
-
|
|
85
|
-
```text
|
|
86
|
-
npx @fraylabs/possible@0.2.0 create <slug>
|
|
87
|
-
npx @fraylabs/possible@0.2.0 validate [directory]
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Publish from the owner's source
|
|
91
|
-
|
|
92
|
-
Possible does not host publisher accounts or own the canonical files. A publisher exposes Outcomes from either:
|
|
93
|
-
|
|
94
|
-
- a public GitHub repository; or
|
|
95
|
-
- `https://publisher.example/.well-known/possible/outcomes.json`.
|
|
96
|
-
|
|
97
|
-
The publisher index is a thin list of manifest locations. Possible snapshots the public source revision and displays it; changing the source creates a new revision rather than rewriting history.
|
|
98
|
-
|
|
99
|
-
```text
|
|
100
|
-
npx @fraylabs/possible@0.2.0 publish [owner/repository | https://publisher.example]
|
|
101
|
-
npx @fraylabs/possible@0.2.0 add <owner/repository | https://publisher.example>
|
|
102
|
-
npx @fraylabs/possible@0.2.0 use <source>@<slug>
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
GitHub publishing requires a clean committed revision so the snapshot is reproducible. No Possible login is required. A publisher-domain source is Official for that domain; other sources are Community unless their ownership follows directly from the public source.
|
|
106
|
-
|
|
107
|
-
## Local bookmarks
|
|
108
|
-
|
|
109
|
-
Use bookmarks only when the user asks:
|
|
110
|
-
|
|
111
|
-
```text
|
|
112
|
-
npx @fraylabs/possible@0.2.0 bookmark add <outcome-slug>
|
|
113
|
-
npx @fraylabs/possible@0.2.0 bookmark list
|
|
114
|
-
npx @fraylabs/possible@0.2.0 bookmark remove <outcome-slug>
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Bookmarks are stored locally in `.possible` and do not require an account.
|
package/src/init.mjs
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { constants } from "node:fs";
|
|
2
|
-
import { copyFile, lstat, mkdir, readFile, readdir } from "node:fs/promises";
|
|
3
|
-
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
const DEFAULT_SOURCE = fileURLToPath(new URL("../assets/possible", import.meta.url));
|
|
7
|
-
const INSTALL_SEGMENTS = [".agents", "skills", "possible"];
|
|
8
|
-
|
|
9
|
-
export class InstallConflictError extends Error {
|
|
10
|
-
constructor(conflicts) {
|
|
11
|
-
super(
|
|
12
|
-
`Possible was not installed because existing files conflict:\n${conflicts
|
|
13
|
-
.map((path) => ` - ${path}`)
|
|
14
|
-
.join("\n")}\nMove or remove the conflicting path, then run the command again.`,
|
|
15
|
-
);
|
|
16
|
-
this.name = "InstallConflictError";
|
|
17
|
-
this.conflicts = conflicts;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const pathExists = async (path) => {
|
|
22
|
-
try {
|
|
23
|
-
return await lstat(path);
|
|
24
|
-
} catch (error) {
|
|
25
|
-
if (error?.code === "ENOENT") return null;
|
|
26
|
-
throw error;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const listTree = async (root, current = "") => {
|
|
31
|
-
const directory = join(root, current);
|
|
32
|
-
const entries = await readdir(directory, { withFileTypes: true });
|
|
33
|
-
const files = [];
|
|
34
|
-
const directories = [];
|
|
35
|
-
|
|
36
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
37
|
-
const child = join(current, entry.name);
|
|
38
|
-
if (entry.isSymbolicLink()) {
|
|
39
|
-
throw new Error(`The packaged Possible skill contains an unsupported symbolic link: ${child}`);
|
|
40
|
-
}
|
|
41
|
-
if (entry.isDirectory()) {
|
|
42
|
-
directories.push(child);
|
|
43
|
-
const nested = await listTree(root, child);
|
|
44
|
-
directories.push(...nested.directories);
|
|
45
|
-
files.push(...nested.files);
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (!entry.isFile()) {
|
|
49
|
-
throw new Error(`The packaged Possible skill contains an unsupported entry: ${child}`);
|
|
50
|
-
}
|
|
51
|
-
files.push(child);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return { directories, files };
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const sameFile = async (left, right) => {
|
|
58
|
-
const [leftContent, rightContent] = await Promise.all([readFile(left), readFile(right)]);
|
|
59
|
-
return leftContent.equals(rightContent);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const displayPath = (projectRoot, path) => relative(projectRoot, path) || ".";
|
|
63
|
-
|
|
64
|
-
const assertInsideProject = (projectRoot, path) => {
|
|
65
|
-
const rel = relative(projectRoot, path);
|
|
66
|
-
if (rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)) return;
|
|
67
|
-
throw new Error(`Refusing to write outside the target project: ${path}`);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const ensureDirectory = async (projectRoot, path) => {
|
|
71
|
-
assertInsideProject(projectRoot, path);
|
|
72
|
-
try {
|
|
73
|
-
await mkdir(path);
|
|
74
|
-
} catch (error) {
|
|
75
|
-
if (error?.code !== "EEXIST") throw error;
|
|
76
|
-
}
|
|
77
|
-
const stats = await lstat(path);
|
|
78
|
-
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
79
|
-
throw new InstallConflictError([displayPath(projectRoot, path)]);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export async function installPossibleSkill({ projectDirectory = process.cwd(), sourceDirectory = DEFAULT_SOURCE } = {}) {
|
|
84
|
-
const projectRoot = resolve(projectDirectory);
|
|
85
|
-
const projectStats = await pathExists(projectRoot);
|
|
86
|
-
if (!projectStats?.isDirectory()) {
|
|
87
|
-
throw new Error(`Target project is not a directory: ${projectRoot}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const sourceRoot = resolve(sourceDirectory);
|
|
91
|
-
const sourceStats = await pathExists(sourceRoot);
|
|
92
|
-
if (!sourceStats?.isDirectory()) {
|
|
93
|
-
throw new Error(`Packaged Possible skill is missing: ${sourceRoot}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const tree = await listTree(sourceRoot);
|
|
97
|
-
const installRoot = join(projectRoot, ...INSTALL_SEGMENTS);
|
|
98
|
-
const directoryPaths = [
|
|
99
|
-
join(projectRoot, INSTALL_SEGMENTS[0]),
|
|
100
|
-
join(projectRoot, ...INSTALL_SEGMENTS.slice(0, 2)),
|
|
101
|
-
installRoot,
|
|
102
|
-
...tree.directories.map((path) => join(installRoot, path)),
|
|
103
|
-
];
|
|
104
|
-
const conflicts = [];
|
|
105
|
-
|
|
106
|
-
for (const path of directoryPaths) {
|
|
107
|
-
assertInsideProject(projectRoot, path);
|
|
108
|
-
const stats = await pathExists(path);
|
|
109
|
-
if (stats && (!stats.isDirectory() || stats.isSymbolicLink())) {
|
|
110
|
-
conflicts.push(displayPath(projectRoot, path));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const missingFiles = [];
|
|
115
|
-
for (const sourceRelativePath of tree.files) {
|
|
116
|
-
const sourcePath = join(sourceRoot, sourceRelativePath);
|
|
117
|
-
const destinationPath = join(installRoot, sourceRelativePath);
|
|
118
|
-
assertInsideProject(projectRoot, destinationPath);
|
|
119
|
-
const stats = await pathExists(destinationPath);
|
|
120
|
-
if (!stats) {
|
|
121
|
-
missingFiles.push({ sourcePath, destinationPath });
|
|
122
|
-
} else if (!stats.isFile() || stats.isSymbolicLink() || !(await sameFile(sourcePath, destinationPath))) {
|
|
123
|
-
conflicts.push(displayPath(projectRoot, destinationPath));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (conflicts.length > 0) {
|
|
128
|
-
throw new InstallConflictError([...new Set(conflicts)].sort());
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
for (const path of directoryPaths) {
|
|
132
|
-
await ensureDirectory(projectRoot, path);
|
|
133
|
-
}
|
|
134
|
-
for (const { sourcePath, destinationPath } of missingFiles) {
|
|
135
|
-
try {
|
|
136
|
-
await copyFile(sourcePath, destinationPath, constants.COPYFILE_EXCL);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
if (error?.code === "EEXIST") {
|
|
139
|
-
throw new InstallConflictError([displayPath(projectRoot, destinationPath)]);
|
|
140
|
-
}
|
|
141
|
-
throw error;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
changed: missingFiles.length > 0,
|
|
147
|
-
filesWritten: missingFiles.length,
|
|
148
|
-
installPath: displayPath(projectRoot, installRoot),
|
|
149
|
-
};
|
|
150
|
-
}
|