flowdular 0.2.0 → 0.2.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/README.md +13 -0
- package/dist/distribution.js +1 -1
- package/dist/index.js +551 -231
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -71,3 +71,16 @@ Release tooling can import the source distribution API through `flowdular/distri
|
|
|
71
71
|
- [Documentation](https://github.com/Flowdular/flowdular/tree/main/docs) · [Source and issues](https://github.com/Flowdular/flowdular) · [Website](https://flowdular.com)
|
|
72
72
|
|
|
73
73
|
Licensed under MIT. Repository documentation and source require repository access while the repositories are private.
|
|
74
|
+
|
|
75
|
+
## Interactive setup
|
|
76
|
+
|
|
77
|
+
Run `pnpm flowdular setup` in a terminal. The wizard offers a local demo, existing
|
|
78
|
+
PostgreSQL connection settings, or a configuration check. Local resets require
|
|
79
|
+
confirmation and a stopped application; custom or hosted databases are refused.
|
|
80
|
+
PostgreSQL URLs are masked and saved to `.env` with TLS verification; this does
|
|
81
|
+
not initialize, reset or connect to the hosted database.
|
|
82
|
+
|
|
83
|
+
Without an interactive terminal, `setup` remains a configuration check. Use
|
|
84
|
+
`setup check --json` for machine-readable results and
|
|
85
|
+
`setup quick --apply --confirm reset-local-auth` for an explicitly authorized
|
|
86
|
+
local reset in a script. Ctrl+C or declining confirmation leaves data unchanged.
|
package/dist/distribution.js
CHANGED
|
@@ -3405,7 +3405,7 @@ import { dirname as dirname6, join as join6, relative as relative5 } from "node:
|
|
|
3405
3405
|
import { createRequire } from "node:module";
|
|
3406
3406
|
import { readFile as readFile5 } from "node:fs/promises";
|
|
3407
3407
|
import { dirname as dirname4, join as join3 } from "node:path";
|
|
3408
|
-
var SDK_VERSION = "0.2.
|
|
3408
|
+
var SDK_VERSION = "0.2.1";
|
|
3409
3409
|
function npmPackage(specifier) {
|
|
3410
3410
|
return specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
|
|
3411
3411
|
}
|
package/dist/index.js
CHANGED
|
@@ -1999,15 +1999,42 @@ function isCapabilityList(values2) {
|
|
|
1999
1999
|
}
|
|
2000
2000
|
function renderOutput(envelope, json2) {
|
|
2001
2001
|
if (json2) return JSON.stringify(envelope, null, 2);
|
|
2002
|
-
if (!envelope.ok)
|
|
2003
|
-
|
|
2002
|
+
if (!envelope.ok) {
|
|
2003
|
+
const heading = `ERROR ${envelope.error?.code ?? "UNKNOWN"}
|
|
2004
2004
|
${envelope.error?.message ?? "Command failed."}`;
|
|
2005
|
+
if (envelope.error?.code !== "DOCTOR_FAILED") return heading;
|
|
2006
|
+
const details = envelope.error.details;
|
|
2007
|
+
if (!Array.isArray(details?.checks)) return heading;
|
|
2008
|
+
const failures = details.checks.filter(
|
|
2009
|
+
(check) => check && check.status === "fail" && typeof check.id === "string" && typeof check.message === "string"
|
|
2010
|
+
);
|
|
2011
|
+
return [
|
|
2012
|
+
heading,
|
|
2013
|
+
...failures.map((check) => `FAIL ${check.id} ${check.message}`)
|
|
2014
|
+
].join("\n");
|
|
2015
|
+
}
|
|
2005
2016
|
const decorate = (body) => [
|
|
2006
2017
|
body,
|
|
2007
2018
|
...envelope.warnings.map((warning) => `WARNING ${warning}`),
|
|
2008
2019
|
...envelope.evidence.map((item) => `EVIDENCE ${item}`)
|
|
2009
2020
|
].join("\n");
|
|
2010
2021
|
const data2 = envelope.data;
|
|
2022
|
+
if (data2?.cancelled === true) return decorate("Setup cancelled.");
|
|
2023
|
+
if (data2?.setup === "postgresql")
|
|
2024
|
+
return decorate(
|
|
2025
|
+
"PostgreSQL settings saved to .env.\nRun pnpm dev, then open http://localhost:4310 to create an account."
|
|
2026
|
+
);
|
|
2027
|
+
if (data2?.setup === "local") {
|
|
2028
|
+
const admin = data2.accounts?.admin;
|
|
2029
|
+
return decorate(
|
|
2030
|
+
[
|
|
2031
|
+
"Local demo is ready.",
|
|
2032
|
+
"Run pnpm dev, then open http://localhost:4310.",
|
|
2033
|
+
...admin?.email ? [`Demo administrator: ${admin.email}`] : [],
|
|
2034
|
+
...admin?.password ? [`Public demo password: ${admin.password}`] : []
|
|
2035
|
+
].join("\n")
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2011
2038
|
if (data2 && Array.isArray(data2.checks)) {
|
|
2012
2039
|
return decorate(
|
|
2013
2040
|
data2.checks.map((check) => {
|
|
@@ -2026,143 +2053,53 @@ ${envelope.error?.message ?? "Command failed."}`;
|
|
|
2026
2053
|
return decorate(JSON.stringify(data2, null, 2));
|
|
2027
2054
|
}
|
|
2028
2055
|
|
|
2029
|
-
//
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
}
|
|
2048
|
-
function assertModuleCompatibility(manifest2, platformVersion = PLATFORM_API_VERSION) {
|
|
2049
|
-
if (!(0, import_semver.valid)(manifest2.version))
|
|
2050
|
-
throw new RegistryError(
|
|
2051
|
-
"MODULE_VERSION_INVALID",
|
|
2052
|
-
`Invalid version for ${manifest2.id}: ${manifest2.version}`
|
|
2053
|
-
);
|
|
2054
|
-
if (manifest2.platformApi !== void 0 && (!(0, import_semver.validRange)(manifest2.platformApi) || platformVersion !== null && !satisfiesModuleVersion(platformVersion, manifest2.platformApi))) {
|
|
2055
|
-
throw new RegistryError(
|
|
2056
|
-
"MODULE_PLATFORM_INCOMPATIBLE",
|
|
2057
|
-
`${manifest2.id} requires platform API ${manifest2.platformApi}; available ${PLATFORM_API_VERSION}.`
|
|
2058
|
-
);
|
|
2059
|
-
}
|
|
2060
|
-
const ids = /* @__PURE__ */ new Set();
|
|
2061
|
-
for (const dependency of manifest2.dependencies) {
|
|
2062
|
-
if (!dependency.range.trim() || !(0, import_semver.validRange)(dependency.range))
|
|
2063
|
-
throw new RegistryError(
|
|
2064
|
-
"MODULE_RANGE_INVALID",
|
|
2065
|
-
`${manifest2.id} declares an invalid range for ${dependency.id}: ${dependency.range}`
|
|
2066
|
-
);
|
|
2067
|
-
if (ids.has(dependency.id))
|
|
2068
|
-
throw new RegistryError(
|
|
2069
|
-
"MODULE_DEPENDENCY_DUPLICATE",
|
|
2070
|
-
`${manifest2.id} declares ${dependency.id} more than once.`
|
|
2071
|
-
);
|
|
2072
|
-
ids.add(dependency.id);
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
function assertModuleDependency(consumer, dependency, version) {
|
|
2076
|
-
if (!satisfiesModuleVersion(version, dependency.range)) {
|
|
2077
|
-
throw new RegistryError(
|
|
2078
|
-
"MODULE_DEPENDENCY_INCOMPATIBLE",
|
|
2079
|
-
`${consumer} requires ${dependency.id} ${dependency.range}; available ${version}.`
|
|
2080
|
-
);
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
// ../kernel/src/module-registry.ts
|
|
2085
|
-
function compareModules(left, right) {
|
|
2086
|
-
return left.manifest.id.localeCompare(right.manifest.id);
|
|
2087
|
-
}
|
|
2088
|
-
function visitModule(id, modules, visiting, visited, ordered) {
|
|
2089
|
-
if (visited.has(id)) return;
|
|
2090
|
-
if (visiting.has(id)) {
|
|
2091
|
-
throw new RegistryError(
|
|
2092
|
-
"MODULE_DEPENDENCY_CYCLE",
|
|
2093
|
-
`Dependency cycle detected at "${id}".`
|
|
2094
|
-
);
|
|
2095
|
-
}
|
|
2096
|
-
const current = modules.get(id);
|
|
2097
|
-
if (!current) {
|
|
2098
|
-
throw new RegistryError(
|
|
2099
|
-
"MODULE_NOT_FOUND",
|
|
2100
|
-
`Module "${id}" is not registered.`
|
|
2101
|
-
);
|
|
2102
|
-
}
|
|
2103
|
-
visiting.add(id);
|
|
2104
|
-
for (const dependency of [...current.manifest.dependencies].sort(
|
|
2105
|
-
(a, b) => a.id.localeCompare(b.id)
|
|
2106
|
-
)) {
|
|
2107
|
-
if (!modules.has(dependency.id)) {
|
|
2108
|
-
throw new RegistryError(
|
|
2109
|
-
"MODULE_DEPENDENCY_MISSING",
|
|
2110
|
-
`Module "${id}" requires missing module "${dependency.id}".`
|
|
2056
|
+
// src/workspace.ts
|
|
2057
|
+
import { access, readFile, realpath } from "node:fs/promises";
|
|
2058
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
2059
|
+
async function findWorkspace(start) {
|
|
2060
|
+
let directory = resolve(start);
|
|
2061
|
+
for (; ; ) {
|
|
2062
|
+
const configPath = resolve(directory, "flowdular.json");
|
|
2063
|
+
try {
|
|
2064
|
+
await access(configPath);
|
|
2065
|
+
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
2066
|
+
return { root: directory, configPath, config };
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
if (error instanceof SyntaxError) throw error;
|
|
2069
|
+
}
|
|
2070
|
+
const parent = dirname(directory);
|
|
2071
|
+
if (parent === directory) {
|
|
2072
|
+
throw new Error(
|
|
2073
|
+
"No flowdular.json was found in this directory or any parent."
|
|
2111
2074
|
);
|
|
2112
2075
|
}
|
|
2113
|
-
|
|
2114
|
-
id,
|
|
2115
|
-
dependency,
|
|
2116
|
-
modules.get(dependency.id).manifest.version
|
|
2117
|
-
);
|
|
2118
|
-
visitModule(dependency.id, modules, visiting, visited, ordered);
|
|
2076
|
+
directory = parent;
|
|
2119
2077
|
}
|
|
2120
|
-
visiting.delete(id);
|
|
2121
|
-
visited.add(id);
|
|
2122
|
-
ordered.push(current);
|
|
2123
2078
|
}
|
|
2124
|
-
function
|
|
2125
|
-
const
|
|
2126
|
-
const
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
if (byId.has(module.manifest.id)) {
|
|
2130
|
-
throw new RegistryError(
|
|
2131
|
-
"MODULE_DUPLICATE",
|
|
2132
|
-
`Module "${module.manifest.id}" is registered more than once.`
|
|
2133
|
-
);
|
|
2134
|
-
}
|
|
2135
|
-
byId.set(module.manifest.id, module);
|
|
2136
|
-
for (const item of module.navigation ?? []) {
|
|
2137
|
-
if (navigationIds.has(item.id)) {
|
|
2138
|
-
throw new RegistryError(
|
|
2139
|
-
"NAVIGATION_DUPLICATE",
|
|
2140
|
-
`Navigation contribution "${item.id}" is registered more than once.`
|
|
2141
|
-
);
|
|
2142
|
-
}
|
|
2143
|
-
navigationIds.add(item.id);
|
|
2144
|
-
}
|
|
2079
|
+
function resolveInside(root, candidate) {
|
|
2080
|
+
const absolute = isAbsolute(candidate) ? resolve(candidate) : resolve(root, candidate);
|
|
2081
|
+
const pathFromRoot = relative(root, absolute);
|
|
2082
|
+
if (pathFromRoot.startsWith("..") || isAbsolute(pathFromRoot)) {
|
|
2083
|
+
throw new Error(`Path escapes the workspace: ${candidate}`);
|
|
2145
2084
|
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2085
|
+
return absolute;
|
|
2086
|
+
}
|
|
2087
|
+
async function resolveExistingInside(root, candidate) {
|
|
2088
|
+
const canonicalRoot = await realpath(root);
|
|
2089
|
+
const canonicalCandidate = await realpath(resolveInside(root, candidate));
|
|
2090
|
+
const pathFromRoot = relative(canonicalRoot, canonicalCandidate);
|
|
2091
|
+
if (pathFromRoot.startsWith("..") || isAbsolute(pathFromRoot)) {
|
|
2092
|
+
throw new Error(`Path escapes the workspace through a link: ${candidate}`);
|
|
2151
2093
|
}
|
|
2152
|
-
return
|
|
2153
|
-
modules: Object.freeze(ordered),
|
|
2154
|
-
get: (id) => byId.get(id),
|
|
2155
|
-
has: (id) => byId.has(id),
|
|
2156
|
-
navigation: (permissions) => Object.freeze(
|
|
2157
|
-
ordered.flatMap((module) => module.navigation ?? []).filter(
|
|
2158
|
-
(item) => !item.permission || permissions?.has(item.permission)
|
|
2159
|
-
).sort(
|
|
2160
|
-
(left, right) => (left.order ?? 1e3) - (right.order ?? 1e3) || left.id.localeCompare(right.id)
|
|
2161
|
-
)
|
|
2162
|
-
)
|
|
2163
|
-
});
|
|
2094
|
+
return canonicalCandidate;
|
|
2164
2095
|
}
|
|
2165
2096
|
|
|
2097
|
+
// src/validation.ts
|
|
2098
|
+
import { readdir, readFile as readFile2 } from "node:fs/promises";
|
|
2099
|
+
import { basename, dirname as dirname2, join, relative as relative2 } from "node:path";
|
|
2100
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
2101
|
+
import { parse as parseYaml } from "yaml";
|
|
2102
|
+
|
|
2166
2103
|
// ../contracts/schemas/blueprint.schema.json
|
|
2167
2104
|
var blueprint_schema_default = {
|
|
2168
2105
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -2985,19 +2922,7 @@ var projectSchema = project_schema_default;
|
|
|
2985
2922
|
var moduleCatalogSchema = module_catalog_schema_default;
|
|
2986
2923
|
var moduleArtifactSchema = module_artifact_schema_default;
|
|
2987
2924
|
|
|
2988
|
-
// src/module-catalog.ts
|
|
2989
|
-
import { readFile as readFile3, stat } from "node:fs/promises";
|
|
2990
|
-
import { dirname as dirname3, resolve as resolve2 } from "node:path";
|
|
2991
|
-
|
|
2992
|
-
// src/module-artifact.ts
|
|
2993
|
-
import { parse as parseYaml2 } from "yaml";
|
|
2994
|
-
import { createHash } from "node:crypto";
|
|
2995
|
-
|
|
2996
2925
|
// src/validation.ts
|
|
2997
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
2998
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
2999
|
-
import Ajv2020 from "ajv/dist/2020.js";
|
|
3000
|
-
import { parse as parseYaml } from "yaml";
|
|
3001
2926
|
var ajv = new Ajv2020({ allErrors: true, strict: true });
|
|
3002
2927
|
var validators = {
|
|
3003
2928
|
application: ajv.compile(projectSchema.properties.application),
|
|
@@ -3020,7 +2945,7 @@ function issuesFrom(errors) {
|
|
|
3020
2945
|
}));
|
|
3021
2946
|
}
|
|
3022
2947
|
async function parseFile(path) {
|
|
3023
|
-
const source = await
|
|
2948
|
+
const source = await readFile2(path, "utf8");
|
|
3024
2949
|
return path.endsWith(".yaml") || path.endsWith(".yml") ? parseYaml(source) : JSON.parse(source);
|
|
3025
2950
|
}
|
|
3026
2951
|
async function validateFile(path, validator) {
|
|
@@ -3084,7 +3009,7 @@ var blueprintFiles = [
|
|
|
3084
3009
|
];
|
|
3085
3010
|
async function validateBlueprint(path) {
|
|
3086
3011
|
const report = await validateFile(path, validators.blueprint);
|
|
3087
|
-
const directory =
|
|
3012
|
+
const directory = dirname2(path);
|
|
3088
3013
|
const entries = new Set(
|
|
3089
3014
|
(await readdir(directory)).filter((name) => basename(name) === name)
|
|
3090
3015
|
);
|
|
@@ -3095,14 +3020,177 @@ async function validateBlueprint(path) {
|
|
|
3095
3020
|
...missing.map((name) => ({
|
|
3096
3021
|
code: "BLUEPRINT_FILE_MISSING",
|
|
3097
3022
|
message: `Required blueprint file is missing: ${name}`,
|
|
3098
|
-
path:
|
|
3023
|
+
path: relative2(process.cwd(), join(directory, name)),
|
|
3099
3024
|
severity: "error"
|
|
3100
3025
|
}))
|
|
3101
3026
|
];
|
|
3102
3027
|
return { ...report, valid: false, issues };
|
|
3103
3028
|
}
|
|
3104
3029
|
|
|
3030
|
+
// src/agent-resources.ts
|
|
3031
|
+
function agentResource(workspace, key) {
|
|
3032
|
+
const defaults = {
|
|
3033
|
+
policy: ".ai/policies/capabilities.yaml",
|
|
3034
|
+
modelRouting: ".ai/policies/model-routing.yaml",
|
|
3035
|
+
blueprints: ".ai/blueprints"
|
|
3036
|
+
};
|
|
3037
|
+
const agent = workspace.config.agent;
|
|
3038
|
+
const path = agent?.[key] ?? defaults[key];
|
|
3039
|
+
if (typeof path !== "string" || !path)
|
|
3040
|
+
throw new Error(`Invalid agent.${key} path.`);
|
|
3041
|
+
return resolveInside(workspace.root, path);
|
|
3042
|
+
}
|
|
3043
|
+
function findBlueprintFiles(workspace) {
|
|
3044
|
+
return findNamedFiles(
|
|
3045
|
+
agentResource(workspace, "blueprints"),
|
|
3046
|
+
"blueprint.json"
|
|
3047
|
+
);
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
// ../kernel/src/errors.ts
|
|
3051
|
+
var RegistryError = class extends Error {
|
|
3052
|
+
code;
|
|
3053
|
+
constructor(code, message) {
|
|
3054
|
+
super(message);
|
|
3055
|
+
this.name = "RegistryError";
|
|
3056
|
+
this.code = code;
|
|
3057
|
+
}
|
|
3058
|
+
};
|
|
3059
|
+
|
|
3060
|
+
// ../kernel/src/module-compatibility.ts
|
|
3061
|
+
var import_semver = __toESM(require_semver2(), 1);
|
|
3062
|
+
var PLATFORM_API_VERSION = "0.1.0";
|
|
3063
|
+
function satisfiesModuleVersion(version, range) {
|
|
3064
|
+
return (0, import_semver.valid)(version) !== null && (0, import_semver.validRange)(range) !== null && (0, import_semver.satisfies)(version, range);
|
|
3065
|
+
}
|
|
3066
|
+
function compareModuleVersions(left, right) {
|
|
3067
|
+
return (0, import_semver.rcompare)(left, right);
|
|
3068
|
+
}
|
|
3069
|
+
function assertModuleCompatibility(manifest2, platformVersion = PLATFORM_API_VERSION) {
|
|
3070
|
+
if (!(0, import_semver.valid)(manifest2.version))
|
|
3071
|
+
throw new RegistryError(
|
|
3072
|
+
"MODULE_VERSION_INVALID",
|
|
3073
|
+
`Invalid version for ${manifest2.id}: ${manifest2.version}`
|
|
3074
|
+
);
|
|
3075
|
+
if (manifest2.platformApi !== void 0 && (!(0, import_semver.validRange)(manifest2.platformApi) || platformVersion !== null && !satisfiesModuleVersion(platformVersion, manifest2.platformApi))) {
|
|
3076
|
+
throw new RegistryError(
|
|
3077
|
+
"MODULE_PLATFORM_INCOMPATIBLE",
|
|
3078
|
+
`${manifest2.id} requires platform API ${manifest2.platformApi}; available ${PLATFORM_API_VERSION}.`
|
|
3079
|
+
);
|
|
3080
|
+
}
|
|
3081
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3082
|
+
for (const dependency of manifest2.dependencies) {
|
|
3083
|
+
if (!dependency.range.trim() || !(0, import_semver.validRange)(dependency.range))
|
|
3084
|
+
throw new RegistryError(
|
|
3085
|
+
"MODULE_RANGE_INVALID",
|
|
3086
|
+
`${manifest2.id} declares an invalid range for ${dependency.id}: ${dependency.range}`
|
|
3087
|
+
);
|
|
3088
|
+
if (ids.has(dependency.id))
|
|
3089
|
+
throw new RegistryError(
|
|
3090
|
+
"MODULE_DEPENDENCY_DUPLICATE",
|
|
3091
|
+
`${manifest2.id} declares ${dependency.id} more than once.`
|
|
3092
|
+
);
|
|
3093
|
+
ids.add(dependency.id);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
function assertModuleDependency(consumer, dependency, version) {
|
|
3097
|
+
if (!satisfiesModuleVersion(version, dependency.range)) {
|
|
3098
|
+
throw new RegistryError(
|
|
3099
|
+
"MODULE_DEPENDENCY_INCOMPATIBLE",
|
|
3100
|
+
`${consumer} requires ${dependency.id} ${dependency.range}; available ${version}.`
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
// ../kernel/src/module-registry.ts
|
|
3106
|
+
function compareModules(left, right) {
|
|
3107
|
+
return left.manifest.id.localeCompare(right.manifest.id);
|
|
3108
|
+
}
|
|
3109
|
+
function visitModule(id, modules, visiting, visited, ordered) {
|
|
3110
|
+
if (visited.has(id)) return;
|
|
3111
|
+
if (visiting.has(id)) {
|
|
3112
|
+
throw new RegistryError(
|
|
3113
|
+
"MODULE_DEPENDENCY_CYCLE",
|
|
3114
|
+
`Dependency cycle detected at "${id}".`
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
const current = modules.get(id);
|
|
3118
|
+
if (!current) {
|
|
3119
|
+
throw new RegistryError(
|
|
3120
|
+
"MODULE_NOT_FOUND",
|
|
3121
|
+
`Module "${id}" is not registered.`
|
|
3122
|
+
);
|
|
3123
|
+
}
|
|
3124
|
+
visiting.add(id);
|
|
3125
|
+
for (const dependency of [...current.manifest.dependencies].sort(
|
|
3126
|
+
(a, b) => a.id.localeCompare(b.id)
|
|
3127
|
+
)) {
|
|
3128
|
+
if (!modules.has(dependency.id)) {
|
|
3129
|
+
throw new RegistryError(
|
|
3130
|
+
"MODULE_DEPENDENCY_MISSING",
|
|
3131
|
+
`Module "${id}" requires missing module "${dependency.id}".`
|
|
3132
|
+
);
|
|
3133
|
+
}
|
|
3134
|
+
assertModuleDependency(
|
|
3135
|
+
id,
|
|
3136
|
+
dependency,
|
|
3137
|
+
modules.get(dependency.id).manifest.version
|
|
3138
|
+
);
|
|
3139
|
+
visitModule(dependency.id, modules, visiting, visited, ordered);
|
|
3140
|
+
}
|
|
3141
|
+
visiting.delete(id);
|
|
3142
|
+
visited.add(id);
|
|
3143
|
+
ordered.push(current);
|
|
3144
|
+
}
|
|
3145
|
+
function createModuleRegistry(input) {
|
|
3146
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3147
|
+
const navigationIds = /* @__PURE__ */ new Set();
|
|
3148
|
+
for (const module of input) {
|
|
3149
|
+
assertModuleCompatibility(module.manifest);
|
|
3150
|
+
if (byId.has(module.manifest.id)) {
|
|
3151
|
+
throw new RegistryError(
|
|
3152
|
+
"MODULE_DUPLICATE",
|
|
3153
|
+
`Module "${module.manifest.id}" is registered more than once.`
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
byId.set(module.manifest.id, module);
|
|
3157
|
+
for (const item of module.navigation ?? []) {
|
|
3158
|
+
if (navigationIds.has(item.id)) {
|
|
3159
|
+
throw new RegistryError(
|
|
3160
|
+
"NAVIGATION_DUPLICATE",
|
|
3161
|
+
`Navigation contribution "${item.id}" is registered more than once.`
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
navigationIds.add(item.id);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
const ordered = [];
|
|
3168
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
3169
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3170
|
+
for (const module of [...input].sort(compareModules)) {
|
|
3171
|
+
visitModule(module.manifest.id, byId, visiting, visited, ordered);
|
|
3172
|
+
}
|
|
3173
|
+
return Object.freeze({
|
|
3174
|
+
modules: Object.freeze(ordered),
|
|
3175
|
+
get: (id) => byId.get(id),
|
|
3176
|
+
has: (id) => byId.has(id),
|
|
3177
|
+
navigation: (permissions) => Object.freeze(
|
|
3178
|
+
ordered.flatMap((module) => module.navigation ?? []).filter(
|
|
3179
|
+
(item) => !item.permission || permissions?.has(item.permission)
|
|
3180
|
+
).sort(
|
|
3181
|
+
(left, right) => (left.order ?? 1e3) - (right.order ?? 1e3) || left.id.localeCompare(right.id)
|
|
3182
|
+
)
|
|
3183
|
+
)
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
// src/module-catalog.ts
|
|
3188
|
+
import { readFile as readFile3, stat } from "node:fs/promises";
|
|
3189
|
+
import { dirname as dirname3, resolve as resolve2 } from "node:path";
|
|
3190
|
+
|
|
3105
3191
|
// src/module-artifact.ts
|
|
3192
|
+
import { parse as parseYaml2 } from "yaml";
|
|
3193
|
+
import { createHash } from "node:crypto";
|
|
3106
3194
|
var MAX_ARTIFACT_BYTES = 48 * 1024 * 1024;
|
|
3107
3195
|
var MAX_SOURCE_BYTES = 32 * 1024 * 1024;
|
|
3108
3196
|
var MAX_FILES = 4096;
|
|
@@ -3261,47 +3349,6 @@ function validateModuleArtifact(value) {
|
|
|
3261
3349
|
return artifact;
|
|
3262
3350
|
}
|
|
3263
3351
|
|
|
3264
|
-
// src/workspace.ts
|
|
3265
|
-
import { access, readFile as readFile2, realpath } from "node:fs/promises";
|
|
3266
|
-
import { dirname as dirname2, isAbsolute, relative as relative2, resolve } from "node:path";
|
|
3267
|
-
async function findWorkspace(start) {
|
|
3268
|
-
let directory = resolve(start);
|
|
3269
|
-
for (; ; ) {
|
|
3270
|
-
const configPath = resolve(directory, "flowdular.json");
|
|
3271
|
-
try {
|
|
3272
|
-
await access(configPath);
|
|
3273
|
-
const config = JSON.parse(await readFile2(configPath, "utf8"));
|
|
3274
|
-
return { root: directory, configPath, config };
|
|
3275
|
-
} catch (error) {
|
|
3276
|
-
if (error instanceof SyntaxError) throw error;
|
|
3277
|
-
}
|
|
3278
|
-
const parent = dirname2(directory);
|
|
3279
|
-
if (parent === directory) {
|
|
3280
|
-
throw new Error(
|
|
3281
|
-
"No flowdular.json was found in this directory or any parent."
|
|
3282
|
-
);
|
|
3283
|
-
}
|
|
3284
|
-
directory = parent;
|
|
3285
|
-
}
|
|
3286
|
-
}
|
|
3287
|
-
function resolveInside(root, candidate) {
|
|
3288
|
-
const absolute = isAbsolute(candidate) ? resolve(candidate) : resolve(root, candidate);
|
|
3289
|
-
const pathFromRoot = relative2(root, absolute);
|
|
3290
|
-
if (pathFromRoot.startsWith("..") || isAbsolute(pathFromRoot)) {
|
|
3291
|
-
throw new Error(`Path escapes the workspace: ${candidate}`);
|
|
3292
|
-
}
|
|
3293
|
-
return absolute;
|
|
3294
|
-
}
|
|
3295
|
-
async function resolveExistingInside(root, candidate) {
|
|
3296
|
-
const canonicalRoot = await realpath(root);
|
|
3297
|
-
const canonicalCandidate = await realpath(resolveInside(root, candidate));
|
|
3298
|
-
const pathFromRoot = relative2(canonicalRoot, canonicalCandidate);
|
|
3299
|
-
if (pathFromRoot.startsWith("..") || isAbsolute(pathFromRoot)) {
|
|
3300
|
-
throw new Error(`Path escapes the workspace through a link: ${candidate}`);
|
|
3301
|
-
}
|
|
3302
|
-
return canonicalCandidate;
|
|
3303
|
-
}
|
|
3304
|
-
|
|
3305
3352
|
// src/module-catalog.ts
|
|
3306
3353
|
var OFFICIAL_CATALOG = "https://raw.githubusercontent.com/Flowdular/official-modules/main/registry/index.json";
|
|
3307
3354
|
var OFFICIAL_PREFIX = "https://raw.githubusercontent.com/Flowdular/official-modules/";
|
|
@@ -3499,7 +3546,7 @@ import { dirname as dirname6, join as join5, relative as relative5 } from "node:
|
|
|
3499
3546
|
import { createRequire } from "node:module";
|
|
3500
3547
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
3501
3548
|
import { dirname as dirname4, join as join2 } from "node:path";
|
|
3502
|
-
var SDK_VERSION = "0.2.
|
|
3549
|
+
var SDK_VERSION = "0.2.1";
|
|
3503
3550
|
var LIBRARIES = /* @__PURE__ */ new Set([
|
|
3504
3551
|
"ai-provider",
|
|
3505
3552
|
"cli-protocol",
|
|
@@ -3591,14 +3638,14 @@ import { createRequire as createRequire2 } from "node:module";
|
|
|
3591
3638
|
import { lstat, readFile as readFile5 } from "node:fs/promises";
|
|
3592
3639
|
import { join as join3, relative as relative3 } from "node:path";
|
|
3593
3640
|
function moduleRoots(workspace) {
|
|
3594
|
-
const
|
|
3595
|
-
if (!Array.isArray(
|
|
3641
|
+
const roots2 = workspace.config.modules?.roots ?? ["modules"];
|
|
3642
|
+
if (!Array.isArray(roots2) || !roots2.length || roots2.some(
|
|
3596
3643
|
(root) => typeof root !== "string" || !root || root.startsWith(".") || root.includes("\\") || root.split("/").some((part) => part === ".." || !part)
|
|
3597
3644
|
))
|
|
3598
3645
|
throw new Error(
|
|
3599
3646
|
"Invalid modules.roots. Use relative workspace directories."
|
|
3600
3647
|
);
|
|
3601
|
-
return
|
|
3648
|
+
return roots2.map((root) => {
|
|
3602
3649
|
const path = resolveInside(workspace.root, root);
|
|
3603
3650
|
if (!relative3(workspace.root, path))
|
|
3604
3651
|
throw new Error("The workspace root cannot be a module root.");
|
|
@@ -4007,10 +4054,10 @@ async function validateModules(workspace, options = {}) {
|
|
|
4007
4054
|
);
|
|
4008
4055
|
}
|
|
4009
4056
|
try {
|
|
4010
|
-
const
|
|
4057
|
+
const registered2 = manifests.map(
|
|
4011
4058
|
(manifest2) => ({ manifest: manifest2 })
|
|
4012
4059
|
);
|
|
4013
|
-
const registry = createModuleRegistry(
|
|
4060
|
+
const registry = createModuleRegistry(registered2);
|
|
4014
4061
|
const enabled = config.modules?.enabled ?? [];
|
|
4015
4062
|
const missingEnabled = enabled.filter((id) => !registry.has(id));
|
|
4016
4063
|
if (missingEnabled.length > 0) {
|
|
@@ -6004,19 +6051,35 @@ function createPgliteCluster(options = {}) {
|
|
|
6004
6051
|
const close = () => {
|
|
6005
6052
|
closePromise ??= (async () => {
|
|
6006
6053
|
if (!databasePromise) return;
|
|
6007
|
-
const instance = await databasePromise;
|
|
6054
|
+
const instance = await databasePromise.catch(() => void 0);
|
|
6008
6055
|
databasePromise = void 0;
|
|
6009
|
-
await instance
|
|
6056
|
+
await instance?.close();
|
|
6010
6057
|
})();
|
|
6011
6058
|
return closePromise;
|
|
6012
6059
|
};
|
|
6013
6060
|
const database = async () => {
|
|
6014
6061
|
databasePromise ??= (async () => {
|
|
6062
|
+
if (options.dataDirectory) {
|
|
6063
|
+
const { mkdir: mkdir6 } = await import("node:fs/promises");
|
|
6064
|
+
await mkdir6(options.dataDirectory, { recursive: true, mode: 448 });
|
|
6065
|
+
}
|
|
6015
6066
|
const created = options.dataDirectory ? await PGlite.create({
|
|
6016
6067
|
dataDir: options.dataDirectory,
|
|
6017
6068
|
parsers: SERVER_PARSERS
|
|
6018
6069
|
}) : await PGlite.create({ parsers: SERVER_PARSERS });
|
|
6019
|
-
|
|
6070
|
+
try {
|
|
6071
|
+
if (options.bootstrap) await created.exec(options.bootstrap);
|
|
6072
|
+
} catch (error) {
|
|
6073
|
+
try {
|
|
6074
|
+
await created.close();
|
|
6075
|
+
} catch (closeError) {
|
|
6076
|
+
throw new AggregateError(
|
|
6077
|
+
[error, closeError],
|
|
6078
|
+
"PGlite bootstrap failed and cleanup also failed."
|
|
6079
|
+
);
|
|
6080
|
+
}
|
|
6081
|
+
throw error;
|
|
6082
|
+
}
|
|
6020
6083
|
return created;
|
|
6021
6084
|
})();
|
|
6022
6085
|
return databasePromise;
|
|
@@ -6394,8 +6457,8 @@ async function loadManifests(workspace) {
|
|
|
6394
6457
|
}
|
|
6395
6458
|
return descriptors;
|
|
6396
6459
|
}
|
|
6397
|
-
function moduleOrder(manifests,
|
|
6398
|
-
const requested = new Set(
|
|
6460
|
+
function moduleOrder(manifests, roots2, includeDependencies) {
|
|
6461
|
+
const requested = new Set(roots2);
|
|
6399
6462
|
const visiting = [];
|
|
6400
6463
|
const visited = /* @__PURE__ */ new Set();
|
|
6401
6464
|
const ordered = [];
|
|
@@ -6739,14 +6802,15 @@ async function runDoctor(workspace) {
|
|
|
6739
6802
|
message: project.valid ? "Project contract is valid." : project.issues.map((issue2) => issue2.message).join("; "),
|
|
6740
6803
|
evidence: relative6(workspace.root, workspace.configPath)
|
|
6741
6804
|
});
|
|
6742
|
-
for (const [id,
|
|
6743
|
-
["policy.capabilities", "
|
|
6744
|
-
["policy.model-routing", "
|
|
6745
|
-
["blueprints.root", "
|
|
6746
|
-
["modules.root", "modules"],
|
|
6747
|
-
["packages.root", "packages"],
|
|
6748
|
-
["platform.root", "platform"]
|
|
6805
|
+
for (const [id, resource] of [
|
|
6806
|
+
["policy.capabilities", agentResource(workspace, "policy")],
|
|
6807
|
+
["policy.model-routing", agentResource(workspace, "modelRouting")],
|
|
6808
|
+
["blueprints.root", agentResource(workspace, "blueprints")],
|
|
6809
|
+
["modules.root", join9(workspace.root, "modules")],
|
|
6810
|
+
...(await sdkModules(workspace)).size ? [] : [["packages.root", join9(workspace.root, "packages")]],
|
|
6811
|
+
["platform.root", join9(workspace.root, "platform")]
|
|
6749
6812
|
]) {
|
|
6813
|
+
const path = relative6(workspace.root, resource);
|
|
6750
6814
|
checks.push({
|
|
6751
6815
|
id,
|
|
6752
6816
|
status: await exists3(join9(workspace.root, path)) ? "pass" : "fail",
|
|
@@ -6763,7 +6827,7 @@ async function runDoctor(workspace) {
|
|
|
6763
6827
|
message: packageJson2.packageManager ? `Pinned package manager: ${packageJson2.packageManager}` : "packageManager must pin pnpm.",
|
|
6764
6828
|
evidence: "package.json"
|
|
6765
6829
|
});
|
|
6766
|
-
const blueprints = await
|
|
6830
|
+
const blueprints = await findBlueprintFiles(workspace);
|
|
6767
6831
|
checks.push({
|
|
6768
6832
|
id: "blueprints.discovered",
|
|
6769
6833
|
status: blueprints.length > 0 ? "pass" : "fail",
|
|
@@ -6792,8 +6856,56 @@ async function runDoctor(workspace) {
|
|
|
6792
6856
|
|
|
6793
6857
|
// src/extensions.ts
|
|
6794
6858
|
import { readFile as readFile11 } from "node:fs/promises";
|
|
6795
|
-
import { dirname as
|
|
6796
|
-
|
|
6859
|
+
import { dirname as dirname10, relative as relative7 } from "node:path";
|
|
6860
|
+
|
|
6861
|
+
// src/module-import.ts
|
|
6862
|
+
import { readFileSync as readFileSync2, realpathSync } from "node:fs";
|
|
6863
|
+
import {
|
|
6864
|
+
createRequire as createRequire4,
|
|
6865
|
+
registerHooks,
|
|
6866
|
+
stripTypeScriptTypes
|
|
6867
|
+
} from "node:module";
|
|
6868
|
+
import { dirname as dirname9, sep as sep2 } from "node:path";
|
|
6869
|
+
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
6870
|
+
var roots = /* @__PURE__ */ new Set();
|
|
6871
|
+
var registered = false;
|
|
6872
|
+
async function importModuleSource(entry, moduleRoot) {
|
|
6873
|
+
roots.add(realpathSync(moduleRoot));
|
|
6874
|
+
try {
|
|
6875
|
+
const require2 = createRequire4(pathToFileURL2(entry));
|
|
6876
|
+
roots.add(
|
|
6877
|
+
realpathSync(dirname9(require2.resolve("@flowdular/sdk/package.json")))
|
|
6878
|
+
);
|
|
6879
|
+
} catch (error) {
|
|
6880
|
+
if (!["MODULE_NOT_FOUND", "ERR_PACKAGE_PATH_NOT_EXPORTED"].includes(
|
|
6881
|
+
error.code ?? ""
|
|
6882
|
+
))
|
|
6883
|
+
throw error;
|
|
6884
|
+
}
|
|
6885
|
+
if (!registered) {
|
|
6886
|
+
registerHooks({
|
|
6887
|
+
load(url, context, nextLoad) {
|
|
6888
|
+
if (!url.startsWith("file:") || !fileURLToPath(url).endsWith(".ts"))
|
|
6889
|
+
return nextLoad(url, context);
|
|
6890
|
+
const path = realpathSync(fileURLToPath(url));
|
|
6891
|
+
if (![...roots].some((root) => path.startsWith(root + sep2)))
|
|
6892
|
+
return nextLoad(url, context);
|
|
6893
|
+
return {
|
|
6894
|
+
format: "module",
|
|
6895
|
+
source: stripTypeScriptTypes(readFileSync2(path, "utf8"), {
|
|
6896
|
+
mode: "transform",
|
|
6897
|
+
sourceUrl: url
|
|
6898
|
+
}),
|
|
6899
|
+
shortCircuit: true
|
|
6900
|
+
};
|
|
6901
|
+
}
|
|
6902
|
+
});
|
|
6903
|
+
registered = true;
|
|
6904
|
+
}
|
|
6905
|
+
return import(pathToFileURL2(entry).href);
|
|
6906
|
+
}
|
|
6907
|
+
|
|
6908
|
+
// src/extensions.ts
|
|
6797
6909
|
var reservedGroups = /* @__PURE__ */ new Set([
|
|
6798
6910
|
"help",
|
|
6799
6911
|
"doctor",
|
|
@@ -6849,7 +6961,10 @@ function commandKey(command) {
|
|
|
6849
6961
|
return JSON.stringify({ path: command.path, capability: command.capability });
|
|
6850
6962
|
}
|
|
6851
6963
|
async function loadCliCommand(loaded) {
|
|
6852
|
-
const imported = await
|
|
6964
|
+
const imported = await importModuleSource(
|
|
6965
|
+
loaded.entry,
|
|
6966
|
+
loaded.moduleRoot
|
|
6967
|
+
);
|
|
6853
6968
|
const extension = imported.default ?? imported.cliExtension;
|
|
6854
6969
|
if (!extension) {
|
|
6855
6970
|
throw new Error(
|
|
@@ -6882,7 +6997,7 @@ async function loadCliExtensions(workspace) {
|
|
|
6882
6997
|
await readFile11(manifestPath, "utf8")
|
|
6883
6998
|
);
|
|
6884
6999
|
if (!enabled.has(manifest2.id) || !manifest2.cli) continue;
|
|
6885
|
-
const moduleRoot =
|
|
7000
|
+
const moduleRoot = dirname10(manifestPath);
|
|
6886
7001
|
const entry = await resolveExistingInside(moduleRoot, manifest2.cli.entry);
|
|
6887
7002
|
const catalogPath = await resolveExistingInside(
|
|
6888
7003
|
moduleRoot,
|
|
@@ -6919,9 +7034,8 @@ async function loadCliExtensions(workspace) {
|
|
|
6919
7034
|
}
|
|
6920
7035
|
|
|
6921
7036
|
// src/migration.ts
|
|
6922
|
-
import { readFile as readFile13 } from "node:fs/promises";
|
|
6923
|
-
import { dirname as
|
|
6924
|
-
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
7037
|
+
import { readFile as readFile13, stat as stat2 } from "node:fs/promises";
|
|
7038
|
+
import { dirname as dirname11, join as join11 } from "node:path";
|
|
6925
7039
|
|
|
6926
7040
|
// src/migration-audit.ts
|
|
6927
7041
|
import { readdir as readdir5, readFile as readFile12 } from "node:fs/promises";
|
|
@@ -7085,13 +7199,17 @@ async function loadMigrationModules(workspace, moduleId) {
|
|
|
7085
7199
|
);
|
|
7086
7200
|
if (!enabled.has(manifest2.id)) continue;
|
|
7087
7201
|
if (moduleId && manifest2.id !== moduleId) continue;
|
|
7088
|
-
const entry = join11(
|
|
7089
|
-
let databaseMigrations;
|
|
7202
|
+
const entry = join11(dirname11(manifestPath), "src/services/migration.ts");
|
|
7090
7203
|
try {
|
|
7091
|
-
|
|
7092
|
-
} catch {
|
|
7093
|
-
continue;
|
|
7204
|
+
await stat2(entry);
|
|
7205
|
+
} catch (error) {
|
|
7206
|
+
if (error.code === "ENOENT") continue;
|
|
7207
|
+
throw error;
|
|
7094
7208
|
}
|
|
7209
|
+
const { databaseMigrations } = await importModuleSource(
|
|
7210
|
+
entry,
|
|
7211
|
+
dirname11(manifestPath)
|
|
7212
|
+
);
|
|
7095
7213
|
if (!Array.isArray(databaseMigrations)) {
|
|
7096
7214
|
unmanaged.push({
|
|
7097
7215
|
moduleId: manifest2.id,
|
|
@@ -7101,7 +7219,7 @@ async function loadMigrationModules(workspace, moduleId) {
|
|
|
7101
7219
|
}
|
|
7102
7220
|
managed.push({
|
|
7103
7221
|
moduleId: manifest2.id,
|
|
7104
|
-
moduleDirectory:
|
|
7222
|
+
moduleDirectory: dirname11(manifestPath),
|
|
7105
7223
|
databaseMigrations
|
|
7106
7224
|
});
|
|
7107
7225
|
}
|
|
@@ -7328,7 +7446,7 @@ import {
|
|
|
7328
7446
|
rm as rm3,
|
|
7329
7447
|
writeFile as writeFile4
|
|
7330
7448
|
} from "node:fs/promises";
|
|
7331
|
-
import { dirname as
|
|
7449
|
+
import { dirname as dirname12, join as join12, relative as relative8, resolve as resolve5 } from "node:path";
|
|
7332
7450
|
import { parse as parseYaml4 } from "yaml";
|
|
7333
7451
|
|
|
7334
7452
|
// src/module-templates.ts
|
|
@@ -8697,7 +8815,7 @@ async function writeScaffold(moduleDirectory, files, target) {
|
|
|
8697
8815
|
for (const [path, source] of files) {
|
|
8698
8816
|
if (target.present.has(path)) continue;
|
|
8699
8817
|
const absolute = join12(moduleDirectory, path);
|
|
8700
|
-
const created = await mkdir4(
|
|
8818
|
+
const created = await mkdir4(dirname12(absolute), { recursive: true });
|
|
8701
8819
|
if (created) createdDirectories.push(created);
|
|
8702
8820
|
await writeFile4(absolute, source, { encoding: "utf8", flag: "wx" });
|
|
8703
8821
|
written.push(absolute);
|
|
@@ -8797,9 +8915,9 @@ import {
|
|
|
8797
8915
|
mkdir as mkdir5,
|
|
8798
8916
|
readFile as readFile15,
|
|
8799
8917
|
rm as rm4,
|
|
8800
|
-
stat as
|
|
8918
|
+
stat as stat3
|
|
8801
8919
|
} from "node:fs/promises";
|
|
8802
|
-
import { dirname as
|
|
8920
|
+
import { dirname as dirname13, join as join13, relative as relative9 } from "node:path";
|
|
8803
8921
|
import { createHash as createHash3 } from "node:crypto";
|
|
8804
8922
|
|
|
8805
8923
|
// ../kernel/src/legacy-local-state.ts
|
|
@@ -8923,7 +9041,7 @@ async function ensurePrivateDirectory(path) {
|
|
|
8923
9041
|
await chmod(path, 448);
|
|
8924
9042
|
}
|
|
8925
9043
|
async function copyPlan(plan) {
|
|
8926
|
-
await ensurePrivateDirectory(
|
|
9044
|
+
await ensurePrivateDirectory(dirname13(plan.destinationDirectory));
|
|
8927
9045
|
await ensurePrivateDirectory(plan.destinationDirectory);
|
|
8928
9046
|
const created = [];
|
|
8929
9047
|
try {
|
|
@@ -8932,14 +9050,14 @@ async function copyPlan(plan) {
|
|
|
8932
9050
|
if (sourceState.isSymbolicLink() || !sourceState.isFile()) {
|
|
8933
9051
|
throw new Error(`${file.name} is no longer a regular file.`);
|
|
8934
9052
|
}
|
|
8935
|
-
const before = await
|
|
9053
|
+
const before = await stat3(file.source);
|
|
8936
9054
|
await copyFile(file.source, file.destination, constants.COPYFILE_EXCL);
|
|
8937
9055
|
created.push(file.destination);
|
|
8938
9056
|
await chmod(file.destination, 384);
|
|
8939
9057
|
const [sourceDigest2, destinationDigest, after] = await Promise.all([
|
|
8940
9058
|
digest(file.source),
|
|
8941
9059
|
digest(file.destination),
|
|
8942
|
-
|
|
9060
|
+
stat3(file.source)
|
|
8943
9061
|
]);
|
|
8944
9062
|
if (sourceDigest2 !== destinationDigest || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
|
8945
9063
|
throw new Error(`Source changed while copying ${file.name}.`);
|
|
@@ -8979,7 +9097,7 @@ function data(plan, applied) {
|
|
|
8979
9097
|
ready: plan.blockers.length === 0,
|
|
8980
9098
|
source: LEGACY_DATA_DIRECTORY,
|
|
8981
9099
|
destination: relative9(
|
|
8982
|
-
|
|
9100
|
+
dirname13(dirname13(plan.destinationDirectory)),
|
|
8983
9101
|
plan.destinationDirectory
|
|
8984
9102
|
),
|
|
8985
9103
|
files: plan.files.map((file) => file.name),
|
|
@@ -9163,7 +9281,7 @@ async function runCommand(arguments_) {
|
|
|
9163
9281
|
"module list|validate [--locked]|sync [--apply]|enable <id> [--apply]|disable <id> [--apply]|new <id> --spec <path> [--apply]",
|
|
9164
9282
|
"migration status [--module <id>]|apply --module <id> [--apply]|verify|new <name> --module <id> [--apply]",
|
|
9165
9283
|
"database reset [--module <id>] [--apply --confirm reset-database]",
|
|
9166
|
-
"setup check|quick [--apply --confirm reset-local-auth]|migrate-state [--apply --confirm migrate-legacy-state]",
|
|
9284
|
+
"setup (interactive)|check|quick [--apply --confirm reset-local-auth]|migrate-state [--apply --confirm migrate-legacy-state]",
|
|
9167
9285
|
...extensionCommands.map((entry) => entry.command.path.join(" "))
|
|
9168
9286
|
],
|
|
9169
9287
|
options: [
|
|
@@ -9281,13 +9399,13 @@ async function runCommand(arguments_) {
|
|
|
9281
9399
|
);
|
|
9282
9400
|
}
|
|
9283
9401
|
if (group === "blueprint" && action === "list") {
|
|
9284
|
-
const files = await
|
|
9402
|
+
const files = await findBlueprintFiles(workspace);
|
|
9285
9403
|
return success({
|
|
9286
9404
|
blueprints: files.map((file) => relative10(workspace.root, file))
|
|
9287
9405
|
});
|
|
9288
9406
|
}
|
|
9289
9407
|
if (group === "blueprint" && action === "validate") {
|
|
9290
|
-
const files = await
|
|
9408
|
+
const files = await findBlueprintFiles(workspace);
|
|
9291
9409
|
const reports = await Promise.all(files.map(validateBlueprint));
|
|
9292
9410
|
const data2 = relativeReports2(workspace.root, reports);
|
|
9293
9411
|
return reports.every((report) => report.valid) ? success(
|
|
@@ -9584,11 +9702,213 @@ async function runProgram(arguments_) {
|
|
|
9584
9702
|
}
|
|
9585
9703
|
}
|
|
9586
9704
|
|
|
9705
|
+
// src/setup-wizard.ts
|
|
9706
|
+
import { stat as stat4 } from "node:fs/promises";
|
|
9707
|
+
|
|
9708
|
+
// src/setup-environment.ts
|
|
9709
|
+
import { randomUUID } from "node:crypto";
|
|
9710
|
+
import { lstat as lstat4, open, readFile as readFile17, rename as rename2, rm as rm5, writeFile as writeFile5 } from "node:fs/promises";
|
|
9711
|
+
import { join as join15 } from "node:path";
|
|
9712
|
+
async function readSetupEnvironment(root) {
|
|
9713
|
+
try {
|
|
9714
|
+
return await readFile17(join15(root, ".env"), "utf8");
|
|
9715
|
+
} catch (error) {
|
|
9716
|
+
if (error.code === "ENOENT") return "";
|
|
9717
|
+
throw error;
|
|
9718
|
+
}
|
|
9719
|
+
}
|
|
9720
|
+
async function saveSetupEnvironment(root, before, updates) {
|
|
9721
|
+
const path = join15(root, ".env");
|
|
9722
|
+
try {
|
|
9723
|
+
if (!(await lstat4(path)).isFile())
|
|
9724
|
+
throw new Error("Setup requires .env to be a regular file.");
|
|
9725
|
+
} catch (error) {
|
|
9726
|
+
if (error.code !== "ENOENT") throw error;
|
|
9727
|
+
}
|
|
9728
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
9729
|
+
if (!/^FD_DATABASE_[A-Z_]+$/.test(key) || /[\r\n'\0]/.test(value))
|
|
9730
|
+
throw new Error("Invalid database setting.");
|
|
9731
|
+
}
|
|
9732
|
+
const lines = before.split(/\r?\n/).filter((line) => {
|
|
9733
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(
|
|
9734
|
+
line
|
|
9735
|
+
);
|
|
9736
|
+
if (!match || !(match[1] in updates)) return true;
|
|
9737
|
+
const value = match[2].trim();
|
|
9738
|
+
if (value.startsWith('"') && !value.endsWith('"') || value.startsWith("'") && !value.endsWith("'"))
|
|
9739
|
+
throw new Error(
|
|
9740
|
+
"Edit multiline database settings in .env before using setup."
|
|
9741
|
+
);
|
|
9742
|
+
return false;
|
|
9743
|
+
});
|
|
9744
|
+
const source = lines.join("\n").trimEnd() + "\n" + Object.entries(updates).map(([key, value]) => `${key}='${value}'`).join("\n") + "\n";
|
|
9745
|
+
const temporary = join15(root, `.env.setup-${randomUUID()}.tmp`);
|
|
9746
|
+
const lockPath = join15(root, ".env.setup.lock");
|
|
9747
|
+
const lock = await open(lockPath, "wx", 384);
|
|
9748
|
+
try {
|
|
9749
|
+
await writeFile5(temporary, source, { mode: 384, flag: "wx" });
|
|
9750
|
+
if (await readSetupEnvironment(root) !== before)
|
|
9751
|
+
throw new Error("The .env file changed during setup. Run setup again.");
|
|
9752
|
+
await rename2(temporary, path);
|
|
9753
|
+
} finally {
|
|
9754
|
+
try {
|
|
9755
|
+
await rm5(temporary, { force: true });
|
|
9756
|
+
} finally {
|
|
9757
|
+
await lock.close();
|
|
9758
|
+
await rm5(lockPath, { force: true });
|
|
9759
|
+
}
|
|
9760
|
+
}
|
|
9761
|
+
}
|
|
9762
|
+
|
|
9763
|
+
// src/setup-wizard.ts
|
|
9764
|
+
var prompts = {
|
|
9765
|
+
async choose() {
|
|
9766
|
+
const { select } = await import("@inquirer/prompts");
|
|
9767
|
+
return select({
|
|
9768
|
+
message: "How would you like to set up this application?",
|
|
9769
|
+
choices: [
|
|
9770
|
+
{
|
|
9771
|
+
name: "Local demo with embedded PostgreSQL (PGlite)",
|
|
9772
|
+
value: "local"
|
|
9773
|
+
},
|
|
9774
|
+
{
|
|
9775
|
+
name: "Configure an existing PostgreSQL database",
|
|
9776
|
+
value: "postgresql"
|
|
9777
|
+
},
|
|
9778
|
+
{ name: "Check configuration only", value: "check" }
|
|
9779
|
+
]
|
|
9780
|
+
});
|
|
9781
|
+
},
|
|
9782
|
+
async confirm(message) {
|
|
9783
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
9784
|
+
return confirm({ message, default: false });
|
|
9785
|
+
},
|
|
9786
|
+
async connection(message) {
|
|
9787
|
+
const { password } = await import("@inquirer/prompts");
|
|
9788
|
+
return password({
|
|
9789
|
+
message,
|
|
9790
|
+
mask: "*",
|
|
9791
|
+
validate: (value) => validConnection(value) ? true : "Use postgres://user:password@host/database without SSL query parameters."
|
|
9792
|
+
});
|
|
9793
|
+
}
|
|
9794
|
+
};
|
|
9795
|
+
function validConnection(value) {
|
|
9796
|
+
if (value.length > 4096 || /[\r\n'\0]/.test(value)) return false;
|
|
9797
|
+
try {
|
|
9798
|
+
const url = new URL(value);
|
|
9799
|
+
return ["postgres:", "postgresql:"].includes(url.protocol) && Boolean(url.hostname && url.username && url.pathname.length > 1) && !["sslmode", "sslcert", "sslkey", "sslrootcert"].some(
|
|
9800
|
+
(key) => url.searchParams.has(key)
|
|
9801
|
+
);
|
|
9802
|
+
} catch {
|
|
9803
|
+
return false;
|
|
9804
|
+
}
|
|
9805
|
+
}
|
|
9806
|
+
async function runSetupWizard(arguments_, io = prompts, execute = runProgram) {
|
|
9807
|
+
try {
|
|
9808
|
+
const flags = new Map(arguments_.flags);
|
|
9809
|
+
flags.delete("apply");
|
|
9810
|
+
flags.delete("confirm");
|
|
9811
|
+
const check = await execute({ positionals: ["setup", "check"], flags });
|
|
9812
|
+
if (!check.ok) return check;
|
|
9813
|
+
const workspace = await findWorkspace(
|
|
9814
|
+
stringFlag(arguments_, "root") ?? process.cwd()
|
|
9815
|
+
);
|
|
9816
|
+
const before = await readSetupEnvironment(workspace.root);
|
|
9817
|
+
const choice = await io.choose();
|
|
9818
|
+
if (choice === "check") return check;
|
|
9819
|
+
if (choice === "postgresql") {
|
|
9820
|
+
const urls = {
|
|
9821
|
+
FD_DATABASE_URL: await io.connection("Runtime role connection URL"),
|
|
9822
|
+
FD_DATABASE_MIGRATOR_URL: await io.connection(
|
|
9823
|
+
"Migration role connection URL"
|
|
9824
|
+
),
|
|
9825
|
+
FD_DATABASE_BACKGROUND_URL: await io.connection(
|
|
9826
|
+
"Background role connection URL"
|
|
9827
|
+
)
|
|
9828
|
+
};
|
|
9829
|
+
if (!Object.values(urls).every(validConnection))
|
|
9830
|
+
return failure(
|
|
9831
|
+
"INVALID_DATABASE_CONFIGURATION",
|
|
9832
|
+
"Invalid PostgreSQL connection settings."
|
|
9833
|
+
);
|
|
9834
|
+
const updates = {
|
|
9835
|
+
...urls,
|
|
9836
|
+
FD_DATABASE_ADAPTER: "postgresql",
|
|
9837
|
+
FD_DATABASE_TLS: "verify-full"
|
|
9838
|
+
};
|
|
9839
|
+
databaseProviderConfigFromEnvironment(
|
|
9840
|
+
{ ...process.env, ...updates, NODE_ENV: "production" },
|
|
9841
|
+
workspace.root
|
|
9842
|
+
);
|
|
9843
|
+
if (!await io.confirm(
|
|
9844
|
+
"Save PostgreSQL settings in .env? This does not reset or modify the hosted database."
|
|
9845
|
+
))
|
|
9846
|
+
return success({ cancelled: true });
|
|
9847
|
+
await saveSetupEnvironment(workspace.root, before, updates);
|
|
9848
|
+
return success(
|
|
9849
|
+
{
|
|
9850
|
+
setup: "postgresql",
|
|
9851
|
+
configured: true,
|
|
9852
|
+
adapter: "postgresql",
|
|
9853
|
+
next: "Run pnpm dev, then open http://localhost:4310 and create an account."
|
|
9854
|
+
},
|
|
9855
|
+
{
|
|
9856
|
+
warnings: [
|
|
9857
|
+
"TLS certificate verification is enabled. The database must provide separate migration, runtime and background roles."
|
|
9858
|
+
]
|
|
9859
|
+
}
|
|
9860
|
+
);
|
|
9861
|
+
}
|
|
9862
|
+
const config = databaseProviderConfigFromEnvironment(
|
|
9863
|
+
process.env,
|
|
9864
|
+
workspace.root
|
|
9865
|
+
);
|
|
9866
|
+
if (config.adapter !== "pglite" || process.env.FD_DATABASE_PGLITE_DIRECTORY || process.env.CORELOOM_DATABASE_PGLITE_DIRECTORY || process.env.FD_DATABASE_URL || process.env.CORELOOM_DATABASE_URL)
|
|
9867
|
+
return failure(
|
|
9868
|
+
"LOCAL_DEMO_UNAVAILABLE",
|
|
9869
|
+
"Local demo setup requires the default embedded database. The configured database will not be reset."
|
|
9870
|
+
);
|
|
9871
|
+
const preview = await execute({ positionals: ["setup", "quick"], flags });
|
|
9872
|
+
if (!preview.ok) return preview;
|
|
9873
|
+
const directory = config.pglite?.dataDirectory;
|
|
9874
|
+
let existing = false;
|
|
9875
|
+
if (directory)
|
|
9876
|
+
try {
|
|
9877
|
+
await stat4(directory);
|
|
9878
|
+
existing = true;
|
|
9879
|
+
} catch (error) {
|
|
9880
|
+
if (error.code !== "ENOENT") throw error;
|
|
9881
|
+
}
|
|
9882
|
+
const message = existing ? `Reset the local demo database in ${directory}? All existing local data will be deleted. Confirm that the app is stopped.` : "Create a local demo with admin@example.com? Confirm that the app is stopped.";
|
|
9883
|
+
if (!await io.confirm(message)) return success({ cancelled: true });
|
|
9884
|
+
if (await readSetupEnvironment(workspace.root) !== before)
|
|
9885
|
+
return failure(
|
|
9886
|
+
"SETUP_CONFIGURATION_CHANGED",
|
|
9887
|
+
"The .env file changed during setup. Run setup again."
|
|
9888
|
+
);
|
|
9889
|
+
flags.set("apply", true);
|
|
9890
|
+
flags.set("confirm", "reset-local-auth");
|
|
9891
|
+
const applied = await execute({ positionals: ["setup", "quick"], flags });
|
|
9892
|
+
return applied.ok ? success(
|
|
9893
|
+
{ ...applied.data, setup: "local" },
|
|
9894
|
+
{ warnings: applied.warnings, evidence: applied.evidence }
|
|
9895
|
+
) : applied;
|
|
9896
|
+
} catch (error) {
|
|
9897
|
+
if (error instanceof Error && ["ExitPromptError", "AbortPromptError"].includes(error.name))
|
|
9898
|
+
return success({ cancelled: true });
|
|
9899
|
+
return failure(
|
|
9900
|
+
"SETUP_FAILED",
|
|
9901
|
+
error instanceof Error ? error.message : "Setup failed."
|
|
9902
|
+
);
|
|
9903
|
+
}
|
|
9904
|
+
}
|
|
9905
|
+
|
|
9587
9906
|
// src/index.ts
|
|
9588
9907
|
var invokedAsProgram = process.argv[1]?.endsWith("/index.ts") || process.argv[1]?.endsWith("/index.js");
|
|
9589
9908
|
if (invokedAsProgram) {
|
|
9590
9909
|
const arguments_ = parseArguments(process.argv.slice(2));
|
|
9591
|
-
const
|
|
9910
|
+
const interactive = arguments_.positionals.length === 1 && arguments_.positionals[0] === "setup" && !arguments_.flags.has("json") && !arguments_.flags.has("help") && process.stdin.isTTY && process.stdout.isTTY;
|
|
9911
|
+
const envelope = await (interactive ? runSetupWizard(arguments_) : runProgram(arguments_));
|
|
9592
9912
|
process.stdout.write(
|
|
9593
9913
|
`${renderOutput(envelope, arguments_.flags.has("json"))}
|
|
9594
9914
|
`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowdular",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"flowdular": "./dist/index.js",
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@electric-sql/pglite": "0.5.8",
|
|
15
|
+
"@inquirer/prompts": "8.7.2",
|
|
15
16
|
"ajv": "8.20.0",
|
|
16
17
|
"pg": "8.23.0",
|
|
17
18
|
"yaml": "2.9.0",
|
|
18
|
-
"@flowdular/sdk": "0.2.
|
|
19
|
+
"@flowdular/sdk": "0.2.1"
|
|
19
20
|
},
|
|
20
21
|
"license": "MIT",
|
|
21
22
|
"repository": {
|