@tetherto/mdk-cli 0.0.0 → 0.12.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/LICENSE +202 -0
- package/README.md +447 -0
- package/dist/commands/add.js +61 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/agent.js +42 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/create.js +261 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/inspect.js +171 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/meta.js +12 -0
- package/dist/commands/meta.js.map +1 -0
- package/dist/commands/onboard.js +447 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/run.js +203 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/validate.js +125 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/add-package.js +148 -0
- package/dist/lib/add-package.js.map +1 -0
- package/dist/lib/app-package.js +182 -0
- package/dist/lib/app-package.js.map +1 -0
- package/dist/lib/app-scaffold.js +221 -0
- package/dist/lib/app-scaffold.js.map +1 -0
- package/dist/lib/app-ui-check.js +178 -0
- package/dist/lib/app-ui-check.js.map +1 -0
- package/dist/lib/app-ui-runner.js +122 -0
- package/dist/lib/app-ui-runner.js.map +1 -0
- package/dist/lib/catalog.js +69 -0
- package/dist/lib/catalog.js.map +1 -0
- package/dist/lib/checkout.js +14 -0
- package/dist/lib/checkout.js.map +1 -0
- package/dist/lib/dashboard-apps.js +71 -0
- package/dist/lib/dashboard-apps.js.map +1 -0
- package/dist/lib/dashboard.js +318 -0
- package/dist/lib/dashboard.js.map +1 -0
- package/dist/lib/detect.js +38 -0
- package/dist/lib/detect.js.map +1 -0
- package/dist/lib/env-file.js +57 -0
- package/dist/lib/env-file.js.map +1 -0
- package/dist/lib/fetch-template.js +33 -0
- package/dist/lib/fetch-template.js.map +1 -0
- package/dist/lib/help.js +22 -0
- package/dist/lib/help.js.map +1 -0
- package/dist/lib/names.js +36 -0
- package/dist/lib/names.js.map +1 -0
- package/dist/lib/npm.js +113 -0
- package/dist/lib/npm.js.map +1 -0
- package/dist/lib/package-source.js +76 -0
- package/dist/lib/package-source.js.map +1 -0
- package/dist/lib/pkg.js +5 -0
- package/dist/lib/pkg.js.map +1 -0
- package/dist/lib/plugin-setup.js +204 -0
- package/dist/lib/plugin-setup.js.map +1 -0
- package/dist/lib/ports.js +31 -0
- package/dist/lib/ports.js.map +1 -0
- package/dist/lib/project.js +297 -0
- package/dist/lib/project.js.map +1 -0
- package/dist/lib/run-lock.js +188 -0
- package/dist/lib/run-lock.js.map +1 -0
- package/dist/lib/runtime.js +683 -0
- package/dist/lib/runtime.js.map +1 -0
- package/dist/lib/scaffold.js +82 -0
- package/dist/lib/scaffold.js.map +1 -0
- package/dist/lib/shutdown.js +53 -0
- package/dist/lib/shutdown.js.map +1 -0
- package/dist/lib/skill.js +38 -0
- package/dist/lib/skill.js.map +1 -0
- package/dist/lib/spec.js +375 -0
- package/dist/lib/spec.js.map +1 -0
- package/dist/lib/status.js +245 -0
- package/dist/lib/status.js.map +1 -0
- package/dist/lib/theme.js +50 -0
- package/dist/lib/theme.js.map +1 -0
- package/dist/lib/worker-package.js +74 -0
- package/dist/lib/worker-package.js.map +1 -0
- package/dist/lib/worker-scaffold.js +122 -0
- package/dist/lib/worker-scaffold.js.map +1 -0
- package/dist/lib/worker-smoke.js +243 -0
- package/dist/lib/worker-smoke.js.map +1 -0
- package/dist/program.js +58 -0
- package/dist/program.js.map +1 -0
- package/package.json +59 -1
- package/templates/app/README.md +66 -0
- package/templates/app/mdk-app.yaml +10 -0
- package/templates/app/package.json +17 -0
- package/templates/app/plugin/controllers/summary.js +36 -0
- package/templates/app/plugin/lib/client.js +11 -0
- package/templates/app/plugin/mdk-plugin.json +16 -0
- package/templates/app-ui/tsconfig.json +29 -0
- package/templates/app-ui/ui-src/components/summary-panel.tsx +24 -0
- package/templates/app-ui/ui-src/data/summary.ts +18 -0
- package/templates/app-ui/ui-src/hooks/use-summary.ts +54 -0
- package/templates/app-ui/ui-src/index.tsx +15 -0
- package/templates/app-ui/ui-src/pages/overview-page.tsx +18 -0
- package/templates/app-ui/ui-src/styles.css +13 -0
- package/templates/app-ui/ui-src/styles.d.ts +3 -0
- package/templates/app-ui/ui-src/vite.config.ts +26 -0
- package/templates/worker/README.md +93 -0
- package/templates/worker/mdk-contract.json +26 -0
- package/templates/worker/mock/server.js +93 -0
- package/templates/worker/package.json +23 -0
- package/templates/worker/smoke.config.js +31 -0
- package/templates/worker/src/client.js +47 -0
- package/templates/worker/src/commands/reboot.js +5 -0
- package/templates/worker/src/commands/set-power-mode.js +10 -0
- package/templates/worker/src/telemetry/hashrate-rt.js +5 -0
- package/templates/worker/src/telemetry/power-mode.js +5 -0
- package/templates/worker/src/telemetry/power.js +5 -0
- package/templates/worker/src/telemetry/temperature.js +5 -0
package/dist/lib/npm.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { isFileLinkedFromProject, isWorkspaceMember } from './project.js';
|
|
5
|
+
/** The npm executable name for the current platform. */
|
|
6
|
+
export function npmBin() {
|
|
7
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Whether npm must be spawned through a shell. Node refuses to spawn a `.cmd`
|
|
11
|
+
* file without one on Windows (EINVAL since the CVE-2024-27980 fix), so win32
|
|
12
|
+
* needs `shell: true`. A shell joins argv unescaped, which is why every caller
|
|
13
|
+
* passes fixed arguments or specs checked against `INSTALL_SPEC_RE` only.
|
|
14
|
+
*/
|
|
15
|
+
export function npmNeedsShell() {
|
|
16
|
+
return process.platform === 'win32';
|
|
17
|
+
}
|
|
18
|
+
/** A bare or scoped npm package name — nothing a shell would interpret. */
|
|
19
|
+
export const NPM_PACKAGE_RE = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
20
|
+
/**
|
|
21
|
+
* What `installPackages` hands to `npm install`: a package name with an optional
|
|
22
|
+
* `@version` / `@range`, or `github:owner/repo[#ref]`. No quote, `%`, `&`, `;`,
|
|
23
|
+
* `$`, space or newline can appear, so a spec is safe to pass through a shell
|
|
24
|
+
* once double-quoted (see `shellArg`); quoting keeps a range's `^ < > |` literal.
|
|
25
|
+
*/
|
|
26
|
+
export const INSTALL_SPEC_RE = /^(?:(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[0-9A-Za-z.~^*<>=|+-]+)?|github:[A-Za-z0-9][\w.-]*\/[\w.-]+(?:#[\w./-]+)?)$/;
|
|
27
|
+
/** A checked spec as one argument: quoted when npm runs through a shell. */
|
|
28
|
+
function shellArg(spec) {
|
|
29
|
+
return npmNeedsShell() ? `"${spec}"` : spec;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Runs `npm install` in `dir` (best-effort). Streams npm's output so progress is
|
|
33
|
+
* visible. Never throws: a missing `package.json`, a missing `npm`, or a
|
|
34
|
+
* non-zero exit is reported via the result so the scaffold still succeeds and
|
|
35
|
+
* the user can install manually.
|
|
36
|
+
*/
|
|
37
|
+
export function installDeps(dir, run = true, extraArgs = []) {
|
|
38
|
+
if (!run)
|
|
39
|
+
return { ok: true, skipped: true, message: 'install skipped (--no-install)' };
|
|
40
|
+
if (!existsSync(join(dir, 'package.json'))) {
|
|
41
|
+
return { ok: true, skipped: true, message: 'no package.json — nothing to install' };
|
|
42
|
+
}
|
|
43
|
+
const res = spawnSync(npmBin(), ['install', ...extraArgs], {
|
|
44
|
+
cwd: dir,
|
|
45
|
+
stdio: 'inherit',
|
|
46
|
+
env: process.env,
|
|
47
|
+
shell: npmNeedsShell(),
|
|
48
|
+
});
|
|
49
|
+
if (res.error) {
|
|
50
|
+
const missing = res.error.code === 'ENOENT';
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
message: missing
|
|
54
|
+
? 'npm was not found on PATH — install dependencies manually.'
|
|
55
|
+
: `npm install failed to start: ${res.error.message}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (typeof res.status === 'number' && res.status !== 0) {
|
|
59
|
+
return { ok: false, message: `npm install exited with code ${res.status}.` };
|
|
60
|
+
}
|
|
61
|
+
return { ok: true };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Adds one or more packages to `dir`'s `dependencies` and installs them
|
|
65
|
+
* (`npm install <pkg>...`), so the runtime can later resolve them from the
|
|
66
|
+
* project's `node_modules`. Same best-effort, never-throw contract as
|
|
67
|
+
* `installDeps`: a missing `npm`, a missing manifest, or a non-zero exit is
|
|
68
|
+
* reported via the result rather than thrown. No-ops when `packages` is empty.
|
|
69
|
+
*/
|
|
70
|
+
export function installPackages(dir, packages, run = true, extraArgs = []) {
|
|
71
|
+
if (!run)
|
|
72
|
+
return { ok: true, skipped: true, message: 'install skipped (--no-install)' };
|
|
73
|
+
if (packages.length === 0) {
|
|
74
|
+
return { ok: true, skipped: true, message: 'no packages to install' };
|
|
75
|
+
}
|
|
76
|
+
if (!existsSync(join(dir, 'package.json'))) {
|
|
77
|
+
return { ok: false, message: 'no package.json — run `mdk onboard` first' };
|
|
78
|
+
}
|
|
79
|
+
const invalid = packages.find((spec) => !INSTALL_SPEC_RE.test(spec));
|
|
80
|
+
if (invalid)
|
|
81
|
+
return { ok: false, message: `refusing to install "${invalid}": not a valid npm package or GitHub spec` };
|
|
82
|
+
const res = spawnSync(npmBin(), ['install', ...extraArgs, ...packages.map(shellArg)], {
|
|
83
|
+
cwd: dir,
|
|
84
|
+
stdio: 'inherit',
|
|
85
|
+
env: process.env,
|
|
86
|
+
shell: npmNeedsShell(),
|
|
87
|
+
});
|
|
88
|
+
if (res.error) {
|
|
89
|
+
const missing = res.error.code === 'ENOENT';
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
message: missing
|
|
93
|
+
? 'npm was not found on PATH — install the plugins manually.'
|
|
94
|
+
: `npm install failed to start: ${res.error.message}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (typeof res.status === 'number' && res.status !== 0) {
|
|
98
|
+
return { ok: false, message: `npm install exited with code ${res.status}.` };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Installs dependencies for a freshly scaffolded component. When the package is
|
|
104
|
+
* a workspace member or already declared as a `file:` dep of the project root,
|
|
105
|
+
* npm must run at the project root so the symlink lands in the project's
|
|
106
|
+
* `node_modules`. Everything else installs in its own directory.
|
|
107
|
+
*/
|
|
108
|
+
export function installScaffold(projectDir, packageDir, run = true) {
|
|
109
|
+
const atRoot = isWorkspaceMember(projectDir, packageDir) || isFileLinkedFromProject(projectDir, packageDir);
|
|
110
|
+
const dir = atRoot ? projectDir : packageDir;
|
|
111
|
+
return { ...installDeps(dir, run), dir };
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=npm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"npm.js","sourceRoot":"","sources":["../../src/lib/npm.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { isFileLinkedFromProject, isWorkspaceMember } from './project.js';\n\nexport interface InstallResult {\n ok: boolean;\n /** Skipped when there is nothing to install (no package.json) or `run: false`. */\n skipped?: boolean;\n message?: string;\n}\n\nexport interface ScaffoldInstall extends InstallResult {\n /** The directory npm actually ran in — the workspace root, or the package. */\n dir: string;\n}\n\n/** The npm executable name for the current platform. */\nexport function npmBin(): string {\n return process.platform === 'win32' ? 'npm.cmd' : 'npm';\n}\n\n/**\n * Whether npm must be spawned through a shell. Node refuses to spawn a `.cmd`\n * file without one on Windows (EINVAL since the CVE-2024-27980 fix), so win32\n * needs `shell: true`. A shell joins argv unescaped, which is why every caller\n * passes fixed arguments or specs checked against `INSTALL_SPEC_RE` only.\n */\nexport function npmNeedsShell(): boolean {\n return process.platform === 'win32';\n}\n\n/** A bare or scoped npm package name — nothing a shell would interpret. */\nexport const NPM_PACKAGE_RE = /^(@[a-z0-9][a-z0-9._-]*\\/)?[a-z0-9][a-z0-9._-]*$/;\n\n/**\n * What `installPackages` hands to `npm install`: a package name with an optional\n * `@version` / `@range`, or `github:owner/repo[#ref]`. No quote, `%`, `&`, `;`,\n * `$`, space or newline can appear, so a spec is safe to pass through a shell\n * once double-quoted (see `shellArg`); quoting keeps a range's `^ < > |` literal.\n */\nexport const INSTALL_SPEC_RE =\n /^(?:(?:@[a-z0-9][a-z0-9._-]*\\/)?[a-z0-9][a-z0-9._-]*(?:@[0-9A-Za-z.~^*<>=|+-]+)?|github:[A-Za-z0-9][\\w.-]*\\/[\\w.-]+(?:#[\\w./-]+)?)$/;\n\n/** A checked spec as one argument: quoted when npm runs through a shell. */\nfunction shellArg(spec: string): string {\n return npmNeedsShell() ? `\"${spec}\"` : spec;\n}\n\n/**\n * Runs `npm install` in `dir` (best-effort). Streams npm's output so progress is\n * visible. Never throws: a missing `package.json`, a missing `npm`, or a\n * non-zero exit is reported via the result so the scaffold still succeeds and\n * the user can install manually.\n */\nexport function installDeps(dir: string, run = true, extraArgs: string[] = []): InstallResult {\n if (!run) return { ok: true, skipped: true, message: 'install skipped (--no-install)' };\n if (!existsSync(join(dir, 'package.json'))) {\n return { ok: true, skipped: true, message: 'no package.json — nothing to install' };\n }\n\n const res = spawnSync(npmBin(), ['install', ...extraArgs], {\n cwd: dir,\n stdio: 'inherit',\n env: process.env,\n shell: npmNeedsShell(),\n });\n\n if (res.error) {\n const missing = (res.error as NodeJS.ErrnoException).code === 'ENOENT';\n return {\n ok: false,\n message: missing\n ? 'npm was not found on PATH — install dependencies manually.'\n : `npm install failed to start: ${res.error.message}`,\n };\n }\n if (typeof res.status === 'number' && res.status !== 0) {\n return { ok: false, message: `npm install exited with code ${res.status}.` };\n }\n return { ok: true };\n}\n\n/**\n * Adds one or more packages to `dir`'s `dependencies` and installs them\n * (`npm install <pkg>...`), so the runtime can later resolve them from the\n * project's `node_modules`. Same best-effort, never-throw contract as\n * `installDeps`: a missing `npm`, a missing manifest, or a non-zero exit is\n * reported via the result rather than thrown. No-ops when `packages` is empty.\n */\nexport function installPackages(dir: string, packages: string[], run = true, extraArgs: string[] = []): InstallResult {\n if (!run) return { ok: true, skipped: true, message: 'install skipped (--no-install)' };\n if (packages.length === 0) {\n return { ok: true, skipped: true, message: 'no packages to install' };\n }\n if (!existsSync(join(dir, 'package.json'))) {\n return { ok: false, message: 'no package.json — run `mdk onboard` first' };\n }\n\n const invalid = packages.find((spec) => !INSTALL_SPEC_RE.test(spec));\n if (invalid) return { ok: false, message: `refusing to install \"${invalid}\": not a valid npm package or GitHub spec` };\n\n const res = spawnSync(npmBin(), ['install', ...extraArgs, ...packages.map(shellArg)], {\n cwd: dir,\n stdio: 'inherit',\n env: process.env,\n shell: npmNeedsShell(),\n });\n\n if (res.error) {\n const missing = (res.error as NodeJS.ErrnoException).code === 'ENOENT';\n return {\n ok: false,\n message: missing\n ? 'npm was not found on PATH — install the plugins manually.'\n : `npm install failed to start: ${res.error.message}`,\n };\n }\n if (typeof res.status === 'number' && res.status !== 0) {\n return { ok: false, message: `npm install exited with code ${res.status}.` };\n }\n return { ok: true };\n}\n\n/**\n * Installs dependencies for a freshly scaffolded component. When the package is\n * a workspace member or already declared as a `file:` dep of the project root,\n * npm must run at the project root so the symlink lands in the project's\n * `node_modules`. Everything else installs in its own directory.\n */\nexport function installScaffold(\n projectDir: string,\n packageDir: string,\n run = true,\n): ScaffoldInstall {\n const atRoot =\n isWorkspaceMember(projectDir, packageDir) || isFileLinkedFromProject(projectDir, packageDir);\n const dir = atRoot ? projectDir : packageDir;\n return { ...installDeps(dir, run), dir };\n}\n"]}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { INSTALL_SPEC_RE } from './npm.js';
|
|
2
|
+
import { isLocalPackagePath } from './spec.js';
|
|
3
|
+
// The owner starts with a letter or digit, so `../x` is never read as a repo;
|
|
4
|
+
// a ref is a branch, tag or SHA (`/` allowed for `release/1.x`).
|
|
5
|
+
const GITHUB_URL = /^(?:git\+)?https:\/\/github\.com\/([A-Za-z0-9][\w.-]*)\/([\w.-]+?)(?:\.git)?(?:#([\w./-]+))?$/;
|
|
6
|
+
const GITHUB_SHORTHAND = /^(?:github:)?([A-Za-z0-9][\w.-]*)\/([\w.-]+?)(?:\.git)?(?:#([\w./-]+))?$/;
|
|
7
|
+
const NPM_NAME = /^[a-z0-9][a-z0-9._~-]*$/;
|
|
8
|
+
function github(owner, repo, ref) {
|
|
9
|
+
return { kind: 'github', installSpec: `github:${owner}/${repo}${ref ? `#${ref}` : ''}` };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Classifies an `mdk add` source:
|
|
13
|
+
*
|
|
14
|
+
* - `github:owner/repo[#ref]`, `owner/repo[#ref]` or `https://github.com/owner/repo[.git][#ref]`
|
|
15
|
+
* - an npm spec: `pkg`, `pkg@1.2.3`, `@scope/pkg[@range]`, with an optional trailing subpath
|
|
16
|
+
* (`@scope/pkg/apps/ops`) naming a directory inside the package.
|
|
17
|
+
*
|
|
18
|
+
* `owner/repo` is read as GitHub, as npm itself does; an unscoped npm package with a
|
|
19
|
+
* subpath is therefore ambiguous and has to be written `pkg@latest/sub`.
|
|
20
|
+
*/
|
|
21
|
+
export function parseSource(raw) {
|
|
22
|
+
const source = raw.trim();
|
|
23
|
+
if (!source)
|
|
24
|
+
throw new Error('Give a package source: an npm package or a GitHub repo (owner/repo).');
|
|
25
|
+
if (isLocalPackagePath(source) || source.startsWith('file:') || source.startsWith('~')) {
|
|
26
|
+
throw new Error(`"${raw}" is a path. \`mdk add\` installs from npm or GitHub; for a package on disk, ` +
|
|
27
|
+
'reference it by path in mdk.yaml (`package: ./apps/<name>`), or scaffold one with `mdk create`.');
|
|
28
|
+
}
|
|
29
|
+
const url = GITHUB_URL.exec(source);
|
|
30
|
+
if (url)
|
|
31
|
+
return github(url[1], url[2], url[3]);
|
|
32
|
+
if (source.startsWith('github:')) {
|
|
33
|
+
const short = GITHUB_SHORTHAND.exec(source);
|
|
34
|
+
if (!short)
|
|
35
|
+
throw new Error(`"${raw}" is not a valid GitHub source (expected github:owner/repo[#ref]).`);
|
|
36
|
+
return github(short[1], short[2], short[3]);
|
|
37
|
+
}
|
|
38
|
+
if (!source.startsWith('@')) {
|
|
39
|
+
const short = GITHUB_SHORTHAND.exec(source);
|
|
40
|
+
if (short && !source.includes('@'))
|
|
41
|
+
return github(short[1], short[2], short[3]);
|
|
42
|
+
}
|
|
43
|
+
const segments = source.split('/');
|
|
44
|
+
const nameSegments = source.startsWith('@') ? 2 : 1;
|
|
45
|
+
const head = segments.slice(0, nameSegments);
|
|
46
|
+
const subpath = segments.slice(nameSegments).join('/');
|
|
47
|
+
if (head.length < nameSegments || (source.startsWith('@') && head[0].length < 2)) {
|
|
48
|
+
throw new Error(`"${raw}" is not a valid npm package (expected @scope/name).`);
|
|
49
|
+
}
|
|
50
|
+
// `name@range` carries the version on the last name segment.
|
|
51
|
+
const last = head[head.length - 1];
|
|
52
|
+
const at = last.indexOf('@');
|
|
53
|
+
const bareLast = at === -1 ? last : last.slice(0, at);
|
|
54
|
+
const range = at === -1 ? '' : last.slice(at);
|
|
55
|
+
const bareHead = [...head.slice(0, -1), bareLast];
|
|
56
|
+
const packageName = bareHead.join('/');
|
|
57
|
+
const nameOnly = bareHead[bareHead.length - 1];
|
|
58
|
+
if (!NPM_NAME.test(nameOnly) || (nameSegments === 2 && !/^@[a-z0-9][a-z0-9._~-]*$/.test(bareHead[0]))) {
|
|
59
|
+
throw new Error(`"${raw}" is not a valid npm package name.`);
|
|
60
|
+
}
|
|
61
|
+
if (at !== -1 && range.length < 2)
|
|
62
|
+
throw new Error(`"${raw}" has an empty version after "@".`);
|
|
63
|
+
if (subpath && !subpath.split('/').every((s) => /^[A-Za-z0-9_][\w.-]*$/.test(s))) {
|
|
64
|
+
throw new Error(`"${raw}" is not a valid package subpath (directory names only, no "..").`);
|
|
65
|
+
}
|
|
66
|
+
if (range && !INSTALL_SPEC_RE.test(`${packageName}${range}`)) {
|
|
67
|
+
throw new Error(`"${raw}" has a version npm install cannot take as one argument (no spaces; use e.g. ^1.2.0).`);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
kind: 'npm',
|
|
71
|
+
installSpec: `${packageName}${range}`,
|
|
72
|
+
packageName,
|
|
73
|
+
...(subpath ? { subpath } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=package-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-source.js","sourceRoot":"","sources":["../../src/lib/package-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAe/C,8EAA8E;AAC9E,iEAAiE;AACjE,MAAM,UAAU,GAAG,+FAA+F,CAAC;AACnH,MAAM,gBAAgB,GAAG,0EAA0E,CAAC;AACpG,MAAM,QAAQ,GAAG,yBAAyB,CAAC;AAE3C,SAAS,MAAM,CAAC,KAAa,EAAE,IAAY,EAAE,GAAY;IACvD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,KAAK,IAAI,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC;AAC3F,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC1B,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,sEAAsE,CAAC,CAAC;IACrG,IAAI,kBAAkB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACvF,MAAM,IAAI,KAAK,CACb,IAAI,GAAG,+EAA+E;YACpF,iGAAiG,CACpG,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,GAAG;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAE,EAAE,GAAG,CAAC,CAAC,CAAE,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAEjD,IAAI,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,IAAI,GAAG,oEAAoE,CAAC,CAAC;QACzG,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAE,EAAE,KAAK,CAAC,CAAC,CAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAE,EAAE,KAAK,CAAC,CAAC,CAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACpF,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,YAAY,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACvD,IAAI,IAAI,CAAC,MAAM,GAAG,YAAY,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,CAAE,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;QAClF,MAAM,IAAI,KAAK,CAAC,IAAI,GAAG,sDAAsD,CAAC,CAAC;IACjF,CAAC;IAED,6DAA6D;IAC7D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;IACpC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7B,MAAM,QAAQ,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IAClD,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACvC,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;IAChD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,KAAK,CAAC,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,EAAE,CAAC;QACvG,MAAM,IAAI,KAAK,CAAC,IAAI,GAAG,oCAAoC,CAAC,CAAC;IAC/D,CAAC;IACD,IAAI,EAAE,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,IAAI,GAAG,mCAAmC,CAAC,CAAC;IAC/F,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACjF,MAAM,IAAI,KAAK,CAAC,IAAI,GAAG,mEAAmE,CAAC,CAAC;IAC9F,CAAC;IAED,IAAI,KAAK,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,WAAW,GAAG,KAAK,EAAE,CAAC,EAAE,CAAC;QAC7D,MAAM,IAAI,KAAK,CAAC,IAAI,GAAG,uFAAuF,CAAC,CAAC;IAClH,CAAC;IAED,OAAO;QACL,IAAI,EAAE,KAAK;QACX,WAAW,EAAE,GAAG,WAAW,GAAG,KAAK,EAAE;QACrC,WAAW;QACX,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChC,CAAC;AACJ,CAAC","sourcesContent":["import { INSTALL_SPEC_RE } from './npm.js';\nimport { isLocalPackagePath } from './spec.js';\n\n/**\n * Where `mdk add` installs a package from: the npm registry or a GitHub repo.\n */\nexport interface PackageSource {\n kind: 'npm' | 'github';\n /** The argument handed to `npm install`. */\n installSpec: string;\n /** Known up front for npm (the package part of the spec); learnt from the install for GitHub. */\n packageName?: string;\n /** A directory inside the installed package (npm packages that ship several apps). */\n subpath?: string;\n}\n\n// The owner starts with a letter or digit, so `../x` is never read as a repo;\n// a ref is a branch, tag or SHA (`/` allowed for `release/1.x`).\nconst GITHUB_URL = /^(?:git\\+)?https:\\/\\/github\\.com\\/([A-Za-z0-9][\\w.-]*)\\/([\\w.-]+?)(?:\\.git)?(?:#([\\w./-]+))?$/;\nconst GITHUB_SHORTHAND = /^(?:github:)?([A-Za-z0-9][\\w.-]*)\\/([\\w.-]+?)(?:\\.git)?(?:#([\\w./-]+))?$/;\nconst NPM_NAME = /^[a-z0-9][a-z0-9._~-]*$/;\n\nfunction github(owner: string, repo: string, ref?: string): PackageSource {\n return { kind: 'github', installSpec: `github:${owner}/${repo}${ref ? `#${ref}` : ''}` };\n}\n\n/**\n * Classifies an `mdk add` source:\n *\n * - `github:owner/repo[#ref]`, `owner/repo[#ref]` or `https://github.com/owner/repo[.git][#ref]`\n * - an npm spec: `pkg`, `pkg@1.2.3`, `@scope/pkg[@range]`, with an optional trailing subpath\n * (`@scope/pkg/apps/ops`) naming a directory inside the package.\n *\n * `owner/repo` is read as GitHub, as npm itself does; an unscoped npm package with a\n * subpath is therefore ambiguous and has to be written `pkg@latest/sub`.\n */\nexport function parseSource(raw: string): PackageSource {\n const source = raw.trim();\n if (!source) throw new Error('Give a package source: an npm package or a GitHub repo (owner/repo).');\n if (isLocalPackagePath(source) || source.startsWith('file:') || source.startsWith('~')) {\n throw new Error(\n `\"${raw}\" is a path. \\`mdk add\\` installs from npm or GitHub; for a package on disk, ` +\n 'reference it by path in mdk.yaml (`package: ./apps/<name>`), or scaffold one with `mdk create`.',\n );\n }\n\n const url = GITHUB_URL.exec(source);\n if (url) return github(url[1]!, url[2]!, url[3]);\n\n if (source.startsWith('github:')) {\n const short = GITHUB_SHORTHAND.exec(source);\n if (!short) throw new Error(`\"${raw}\" is not a valid GitHub source (expected github:owner/repo[#ref]).`);\n return github(short[1]!, short[2]!, short[3]);\n }\n\n if (!source.startsWith('@')) {\n const short = GITHUB_SHORTHAND.exec(source);\n if (short && !source.includes('@')) return github(short[1]!, short[2]!, short[3]);\n }\n\n const segments = source.split('/');\n const nameSegments = source.startsWith('@') ? 2 : 1;\n const head = segments.slice(0, nameSegments);\n const subpath = segments.slice(nameSegments).join('/');\n if (head.length < nameSegments || (source.startsWith('@') && head[0]!.length < 2)) {\n throw new Error(`\"${raw}\" is not a valid npm package (expected @scope/name).`);\n }\n\n // `name@range` carries the version on the last name segment.\n const last = head[head.length - 1]!;\n const at = last.indexOf('@');\n const bareLast = at === -1 ? last : last.slice(0, at);\n const range = at === -1 ? '' : last.slice(at);\n const bareHead = [...head.slice(0, -1), bareLast];\n const packageName = bareHead.join('/');\n const nameOnly = bareHead[bareHead.length - 1]!;\n if (!NPM_NAME.test(nameOnly) || (nameSegments === 2 && !/^@[a-z0-9][a-z0-9._~-]*$/.test(bareHead[0]!))) {\n throw new Error(`\"${raw}\" is not a valid npm package name.`);\n }\n if (at !== -1 && range.length < 2) throw new Error(`\"${raw}\" has an empty version after \"@\".`);\n if (subpath && !subpath.split('/').every((s) => /^[A-Za-z0-9_][\\w.-]*$/.test(s))) {\n throw new Error(`\"${raw}\" is not a valid package subpath (directory names only, no \"..\").`);\n }\n\n if (range && !INSTALL_SPEC_RE.test(`${packageName}${range}`)) {\n throw new Error(`\"${raw}\" has a version npm install cannot take as one argument (no spaces; use e.g. ^1.2.0).`);\n }\n\n return {\n kind: 'npm',\n installSpec: `${packageName}${range}`,\n packageName,\n ...(subpath ? { subpath } : {}),\n };\n}\n"]}
|
package/dist/lib/pkg.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkg.js","sourceRoot":"","sources":["../../src/lib/pkg.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAQ/C,sEAAsE;AACtE,MAAM,CAAC,MAAM,GAAG,GAAG,OAAO,CAAC,oBAAoB,CAAgB,CAAC","sourcesContent":["import { createRequire } from 'node:module';\n\nconst require = createRequire(import.meta.url);\n\ninterface PackageJson {\n name: string;\n version: string;\n description?: string;\n}\n\n// dist/lib/pkg.js -> ../../package.json resolves to the package root.\nexport const pkg = require('../../package.json') as PackageJson;\n"]}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { isMap, isSeq, parseDocument } from 'yaml';
|
|
4
|
+
import { confirm, isCancel, password, text } from '@clack/prompts';
|
|
5
|
+
import { shortName } from './spec.js';
|
|
6
|
+
const SETUP_TYPES = new Set(['string', 'secret', 'boolean', 'json']);
|
|
7
|
+
/** Key segments that would reach `Object.prototype` when the dot-path is applied. */
|
|
8
|
+
const FORBIDDEN_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
9
|
+
/** True for a dot-path whose segments are all non-empty and none is forbidden. */
|
|
10
|
+
function isSafeKey(key) {
|
|
11
|
+
return key.split('.').every((part) => part !== '' && !FORBIDDEN_SEGMENTS.has(part));
|
|
12
|
+
}
|
|
13
|
+
class PluginSetupError extends Error {
|
|
14
|
+
}
|
|
15
|
+
function fail(message) {
|
|
16
|
+
throw new PluginSetupError(message);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Reads and validates the `setup` questions of an app package directory, from
|
|
20
|
+
* `<pkgDir>/plugin/mdk-plugin.json` if present, else `<pkgDir>/mdk-plugin.json`.
|
|
21
|
+
* Returns `[]` when the package has no manifest or declares no questions;
|
|
22
|
+
* throws a `PluginSetupError` naming the offender when the block is malformed,
|
|
23
|
+
* so a broken manifest surfaces as a skippable warning rather than bad prompts.
|
|
24
|
+
*/
|
|
25
|
+
export function readSetupQuestions(pkgDir) {
|
|
26
|
+
const manifestPath = [join(pkgDir, 'plugin', 'mdk-plugin.json'), join(pkgDir, 'mdk-plugin.json')].find(existsSync);
|
|
27
|
+
if (!manifestPath)
|
|
28
|
+
return [];
|
|
29
|
+
let manifest;
|
|
30
|
+
try {
|
|
31
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
fail(`${manifestPath} is not valid JSON.`);
|
|
35
|
+
}
|
|
36
|
+
const setup = manifest.setup;
|
|
37
|
+
if (setup == null)
|
|
38
|
+
return [];
|
|
39
|
+
if (!Array.isArray(setup))
|
|
40
|
+
fail(`${manifestPath}: \`setup\` must be an array.`);
|
|
41
|
+
return setup.map((raw, i) => {
|
|
42
|
+
const q = raw;
|
|
43
|
+
if (typeof q?.key !== 'string' || !q.key)
|
|
44
|
+
fail(`${manifestPath}: setup[${i}].key must be a non-empty string.`);
|
|
45
|
+
if (!isSafeKey(q.key)) {
|
|
46
|
+
fail(`${manifestPath}: setup[${i}].key must be a dot-path without empty, __proto__, constructor or prototype segments.`);
|
|
47
|
+
}
|
|
48
|
+
if (typeof q.prompt !== 'string' || !q.prompt)
|
|
49
|
+
fail(`${manifestPath}: setup[${i}].prompt must be a non-empty string.`);
|
|
50
|
+
const type = q.type ?? 'string';
|
|
51
|
+
if (typeof type !== 'string' || !SETUP_TYPES.has(type)) {
|
|
52
|
+
fail(`${manifestPath}: setup[${i}].type must be one of string, secret, boolean, json.`);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
key: q.key,
|
|
56
|
+
prompt: q.prompt,
|
|
57
|
+
type: type,
|
|
58
|
+
required: q.required === true,
|
|
59
|
+
default: typeof q.default === 'string' || typeof q.default === 'boolean' ? q.default : undefined,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Asks the declared questions via clack — `string` → text, `secret` → masked
|
|
65
|
+
* password, `boolean` → confirm, `json` → text validated as JSON and stored
|
|
66
|
+
* parsed (so structured config like role → permission maps stays structured
|
|
67
|
+
* in the spec) — and returns answers keyed by dot-path. Empty optional
|
|
68
|
+
* answers are omitted so the spec stays free of `""` noise and plugin
|
|
69
|
+
* defaults keep applying.
|
|
70
|
+
*/
|
|
71
|
+
export async function promptSetup(questions, onCancel) {
|
|
72
|
+
const answers = {};
|
|
73
|
+
for (const q of questions) {
|
|
74
|
+
let value;
|
|
75
|
+
if (q.type === 'boolean') {
|
|
76
|
+
value = await confirm({ message: q.prompt, initialValue: q.default === true });
|
|
77
|
+
}
|
|
78
|
+
else if (q.type === 'json') {
|
|
79
|
+
value = await text({
|
|
80
|
+
message: q.prompt,
|
|
81
|
+
validate: (v) => {
|
|
82
|
+
const s = v?.trim();
|
|
83
|
+
if (!s)
|
|
84
|
+
return q.required ? 'Required.' : undefined;
|
|
85
|
+
try {
|
|
86
|
+
JSON.parse(s);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return 'Enter valid JSON.';
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
...(typeof q.default === 'string'
|
|
94
|
+
? { placeholder: q.default, defaultValue: q.default, initialValue: q.default }
|
|
95
|
+
: q.required
|
|
96
|
+
? {}
|
|
97
|
+
: { placeholder: 'Enter to keep the plugin defaults' }),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const validate = (v) => q.required && !v?.trim() ? 'Required.' : undefined;
|
|
102
|
+
value =
|
|
103
|
+
q.type === 'secret'
|
|
104
|
+
? await password({ message: q.prompt, validate })
|
|
105
|
+
: await text({
|
|
106
|
+
message: q.prompt,
|
|
107
|
+
validate,
|
|
108
|
+
...(typeof q.default === 'string'
|
|
109
|
+
? { placeholder: q.default, defaultValue: q.default, initialValue: q.default }
|
|
110
|
+
: {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (isCancel(value))
|
|
114
|
+
onCancel();
|
|
115
|
+
if (q.type === 'boolean') {
|
|
116
|
+
answers[q.key] = value === true;
|
|
117
|
+
}
|
|
118
|
+
else if (q.type === 'json') {
|
|
119
|
+
const s = typeof value === 'string' ? value.trim() : '';
|
|
120
|
+
if (s)
|
|
121
|
+
answers[q.key] = JSON.parse(s);
|
|
122
|
+
}
|
|
123
|
+
else if (typeof value === 'string' && value.trim()) {
|
|
124
|
+
answers[q.key] = value.trim();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return answers;
|
|
128
|
+
}
|
|
129
|
+
/** Sets each dot-path answer into `config`, creating nested mappings on the way. */
|
|
130
|
+
export function applySetupAnswers(config, answers) {
|
|
131
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
132
|
+
if (!isSafeKey(key))
|
|
133
|
+
throw new PluginSetupError(`Unsafe setup key "${key}".`);
|
|
134
|
+
const parts = key.split('.');
|
|
135
|
+
let node = config;
|
|
136
|
+
for (const part of parts.slice(0, -1)) {
|
|
137
|
+
if (typeof node[part] !== 'object' || node[part] === null || Array.isArray(node[part])) {
|
|
138
|
+
node[part] = {};
|
|
139
|
+
}
|
|
140
|
+
node = node[part];
|
|
141
|
+
}
|
|
142
|
+
node[parts.at(-1)] = value;
|
|
143
|
+
}
|
|
144
|
+
return config;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Writes setup answers into `<projectDir>/mdk.yaml` under the app's own
|
|
148
|
+
* entry in `spec.apps[].config`, preserving the file's
|
|
149
|
+
* formatting/comments (edits the parsed document, same as the scaffold's
|
|
150
|
+
* stack-file update). Used for apps whose manifest only becomes readable
|
|
151
|
+
* after `npm install` — the spec file is already on disk by then.
|
|
152
|
+
*/
|
|
153
|
+
export function applySetupAnswersToStackFile(projectDir, pluginPkg, answers) {
|
|
154
|
+
const specPath = resolve(projectDir, 'mdk.yaml');
|
|
155
|
+
if (!existsSync(specPath))
|
|
156
|
+
return false;
|
|
157
|
+
try {
|
|
158
|
+
const doc = parseDocument(readFileSync(specPath, 'utf8'));
|
|
159
|
+
const plugins = doc.getIn(['spec', 'apps']);
|
|
160
|
+
if (!isSeq(plugins))
|
|
161
|
+
return false;
|
|
162
|
+
const idx = plugins.items.findIndex((item) => isMap(item) && item.get('package') === pluginPkg);
|
|
163
|
+
if (idx === -1)
|
|
164
|
+
return false;
|
|
165
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
166
|
+
if (!isSafeKey(key))
|
|
167
|
+
return false;
|
|
168
|
+
// createNode so structured answers (json questions) serialize as real
|
|
169
|
+
// YAML mappings/sequences rather than JS object noise.
|
|
170
|
+
doc.setIn(['spec', 'apps', idx, 'config', ...key.split('.')], doc.createNode(value));
|
|
171
|
+
}
|
|
172
|
+
writeFileSync(specPath, doc.toString(), 'utf8');
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Env var holding a plugin's secret setup answer: `MDK_<PLUGIN>_<KEY>`. */
|
|
180
|
+
export function secretEnvName(pluginPkg, key) {
|
|
181
|
+
const part = (s) => s.toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
182
|
+
return `MDK_${part(shortName(pluginPkg))}_${part(key)}`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Keeps `secret` answers out of `mdk.yaml`, which is meant to be committed:
|
|
186
|
+
* each one is replaced by a `${MDK_<PLUGIN>_<KEY>}` placeholder (resolved by
|
|
187
|
+
* `loadStackSpec`) and returned in `env` for the caller to write into the
|
|
188
|
+
* project's gitignored `.env` (see `writeProjectEnv`).
|
|
189
|
+
*/
|
|
190
|
+
export function externalizeSecrets(pluginPkg, questions, answers) {
|
|
191
|
+
const out = { ...answers };
|
|
192
|
+
const env = {};
|
|
193
|
+
for (const q of questions) {
|
|
194
|
+
const value = answers[q.key];
|
|
195
|
+
if (q.type !== 'secret' || typeof value !== 'string')
|
|
196
|
+
continue;
|
|
197
|
+
const name = secretEnvName(pluginPkg, q.key);
|
|
198
|
+
env[name] = value;
|
|
199
|
+
out[q.key] = `\${${name}}`;
|
|
200
|
+
}
|
|
201
|
+
return { answers: out, env };
|
|
202
|
+
}
|
|
203
|
+
export { PluginSetupError };
|
|
204
|
+
//# sourceMappingURL=plugin-setup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-setup.js","sourceRoot":"","sources":["../../src/lib/plugin-setup.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { isMap, isSeq, parseDocument } from 'yaml';\nimport { confirm, isCancel, password, text } from '@clack/prompts';\nimport { shortName } from './spec.js';\n\n/**\n * One interactive setup question an app declares in its\n * `mdk-plugin.json` under `setup`. `key` is a dot-path under the app's own\n * `config` block in `spec.apps[]` — delivered to that app's runtime alone,\n * merged over the `config` defaults in its `mdk-app.yaml`.\n */\nexport interface SetupQuestion {\n key: string;\n prompt: string;\n type: 'string' | 'secret' | 'boolean' | 'json';\n required?: boolean;\n default?: string | boolean;\n}\n\nconst SETUP_TYPES = new Set(['string', 'secret', 'boolean', 'json']);\n\n/** Key segments that would reach `Object.prototype` when the dot-path is applied. */\nconst FORBIDDEN_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype']);\n\n/** True for a dot-path whose segments are all non-empty and none is forbidden. */\nfunction isSafeKey(key: string): boolean {\n return key.split('.').every((part) => part !== '' && !FORBIDDEN_SEGMENTS.has(part));\n}\n\nclass PluginSetupError extends Error {}\n\nfunction fail(message: string): never {\n throw new PluginSetupError(message);\n}\n\n/**\n * Reads and validates the `setup` questions of an app package directory, from\n * `<pkgDir>/plugin/mdk-plugin.json` if present, else `<pkgDir>/mdk-plugin.json`.\n * Returns `[]` when the package has no manifest or declares no questions;\n * throws a `PluginSetupError` naming the offender when the block is malformed,\n * so a broken manifest surfaces as a skippable warning rather than bad prompts.\n */\nexport function readSetupQuestions(pkgDir: string): SetupQuestion[] {\n const manifestPath = [join(pkgDir, 'plugin', 'mdk-plugin.json'), join(pkgDir, 'mdk-plugin.json')].find(existsSync);\n if (!manifestPath) return [];\n\n let manifest: Record<string, unknown>;\n try {\n manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;\n } catch {\n fail(`${manifestPath} is not valid JSON.`);\n }\n\n const setup = manifest.setup;\n if (setup == null) return [];\n if (!Array.isArray(setup)) fail(`${manifestPath}: \\`setup\\` must be an array.`);\n\n return setup.map((raw, i) => {\n const q = raw as Record<string, unknown>;\n if (typeof q?.key !== 'string' || !q.key) fail(`${manifestPath}: setup[${i}].key must be a non-empty string.`);\n if (!isSafeKey(q.key)) {\n fail(`${manifestPath}: setup[${i}].key must be a dot-path without empty, __proto__, constructor or prototype segments.`);\n }\n if (typeof q.prompt !== 'string' || !q.prompt) fail(`${manifestPath}: setup[${i}].prompt must be a non-empty string.`);\n const type = q.type ?? 'string';\n if (typeof type !== 'string' || !SETUP_TYPES.has(type)) {\n fail(`${manifestPath}: setup[${i}].type must be one of string, secret, boolean, json.`);\n }\n return {\n key: q.key,\n prompt: q.prompt,\n type: type as SetupQuestion['type'],\n required: q.required === true,\n default: typeof q.default === 'string' || typeof q.default === 'boolean' ? q.default : undefined,\n };\n });\n}\n\n/**\n * Asks the declared questions via clack — `string` → text, `secret` → masked\n * password, `boolean` → confirm, `json` → text validated as JSON and stored\n * parsed (so structured config like role → permission maps stays structured\n * in the spec) — and returns answers keyed by dot-path. Empty optional\n * answers are omitted so the spec stays free of `\"\"` noise and plugin\n * defaults keep applying.\n */\nexport async function promptSetup(\n questions: SetupQuestion[],\n onCancel: () => never,\n): Promise<Record<string, unknown>> {\n const answers: Record<string, unknown> = {};\n for (const q of questions) {\n let value: unknown;\n if (q.type === 'boolean') {\n value = await confirm({ message: q.prompt, initialValue: q.default === true });\n } else if (q.type === 'json') {\n value = await text({\n message: q.prompt,\n validate: (v) => {\n const s = v?.trim();\n if (!s) return q.required ? 'Required.' : undefined;\n try {\n JSON.parse(s);\n return undefined;\n } catch {\n return 'Enter valid JSON.';\n }\n },\n ...(typeof q.default === 'string'\n ? { placeholder: q.default, defaultValue: q.default, initialValue: q.default }\n : q.required\n ? {}\n : { placeholder: 'Enter to keep the plugin defaults' }),\n });\n } else {\n const validate = (v: string | undefined): string | undefined =>\n q.required && !v?.trim() ? 'Required.' : undefined;\n value =\n q.type === 'secret'\n ? await password({ message: q.prompt, validate })\n : await text({\n message: q.prompt,\n validate,\n ...(typeof q.default === 'string'\n ? { placeholder: q.default, defaultValue: q.default, initialValue: q.default }\n : {}),\n });\n }\n if (isCancel(value)) onCancel();\n if (q.type === 'boolean') {\n answers[q.key] = value === true;\n } else if (q.type === 'json') {\n const s = typeof value === 'string' ? value.trim() : '';\n if (s) answers[q.key] = JSON.parse(s);\n } else if (typeof value === 'string' && value.trim()) {\n answers[q.key] = value.trim();\n }\n }\n return answers;\n}\n\n/** Sets each dot-path answer into `config`, creating nested mappings on the way. */\nexport function applySetupAnswers(\n config: Record<string, unknown>,\n answers: Record<string, unknown>,\n): Record<string, unknown> {\n for (const [key, value] of Object.entries(answers)) {\n if (!isSafeKey(key)) throw new PluginSetupError(`Unsafe setup key \"${key}\".`);\n const parts = key.split('.');\n let node = config;\n for (const part of parts.slice(0, -1)) {\n if (typeof node[part] !== 'object' || node[part] === null || Array.isArray(node[part])) {\n node[part] = {};\n }\n node = node[part] as Record<string, unknown>;\n }\n node[parts.at(-1) as string] = value;\n }\n return config;\n}\n\n/**\n * Writes setup answers into `<projectDir>/mdk.yaml` under the app's own\n * entry in `spec.apps[].config`, preserving the file's\n * formatting/comments (edits the parsed document, same as the scaffold's\n * stack-file update). Used for apps whose manifest only becomes readable\n * after `npm install` — the spec file is already on disk by then.\n */\nexport function applySetupAnswersToStackFile(\n projectDir: string,\n pluginPkg: string,\n answers: Record<string, unknown>,\n): boolean {\n const specPath = resolve(projectDir, 'mdk.yaml');\n if (!existsSync(specPath)) return false;\n\n try {\n const doc = parseDocument(readFileSync(specPath, 'utf8'));\n const plugins = doc.getIn(['spec', 'apps']);\n if (!isSeq(plugins)) return false;\n const idx = plugins.items.findIndex((item) => isMap(item) && item.get('package') === pluginPkg);\n if (idx === -1) return false;\n\n for (const [key, value] of Object.entries(answers)) {\n if (!isSafeKey(key)) return false;\n // createNode so structured answers (json questions) serialize as real\n // YAML mappings/sequences rather than JS object noise.\n doc.setIn(\n ['spec', 'apps', idx, 'config', ...key.split('.')],\n doc.createNode(value),\n );\n }\n writeFileSync(specPath, doc.toString(), 'utf8');\n return true;\n } catch {\n return false;\n }\n}\n\n/** Env var holding a plugin's secret setup answer: `MDK_<PLUGIN>_<KEY>`. */\nexport function secretEnvName(pluginPkg: string, key: string): string {\n const part = (s: string): string => s.toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');\n return `MDK_${part(shortName(pluginPkg))}_${part(key)}`;\n}\n\n/**\n * Keeps `secret` answers out of `mdk.yaml`, which is meant to be committed:\n * each one is replaced by a `${MDK_<PLUGIN>_<KEY>}` placeholder (resolved by\n * `loadStackSpec`) and returned in `env` for the caller to write into the\n * project's gitignored `.env` (see `writeProjectEnv`).\n */\nexport function externalizeSecrets(\n pluginPkg: string,\n questions: SetupQuestion[],\n answers: Record<string, unknown>,\n): { answers: Record<string, unknown>; env: Record<string, string> } {\n const out: Record<string, unknown> = { ...answers };\n const env: Record<string, string> = {};\n for (const q of questions) {\n const value = answers[q.key];\n if (q.type !== 'secret' || typeof value !== 'string') continue;\n const name = secretEnvName(pluginPkg, q.key);\n env[name] = value;\n out[q.key] = `\\${${name}}`;\n }\n return { answers: out, env };\n}\n\nexport { PluginSetupError };\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
/** Loopback host every local mock and component binds to unless told otherwise. */
|
|
3
|
+
export const DEFAULT_HOST = '127.0.0.1';
|
|
4
|
+
/**
|
|
5
|
+
* Whether `port` can be bound on `host` right now, tested by actually binding it
|
|
6
|
+
* (the only answer the OS will give honestly — scanning `lsof` output would miss
|
|
7
|
+
* sockets owned by other users and races either way).
|
|
8
|
+
*/
|
|
9
|
+
export function isPortFree(port, host = DEFAULT_HOST) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
const probe = createServer();
|
|
12
|
+
probe.once('error', () => resolve(false));
|
|
13
|
+
probe.once('listening', () => probe.close(() => resolve(true)));
|
|
14
|
+
probe.listen(port, host);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* First bindable port at or after `start`, skipping any already handed out in
|
|
19
|
+
* this run. Callers scan a small window — a machine with 200 consecutive busy
|
|
20
|
+
* ports in this range has a bigger problem than port selection.
|
|
21
|
+
*/
|
|
22
|
+
export async function findFreePort({ start, host = DEFAULT_HOST, taken, limit = 200, }) {
|
|
23
|
+
for (let port = start; port < start + limit && port <= 65535; port++) {
|
|
24
|
+
if (taken?.has(port))
|
|
25
|
+
continue;
|
|
26
|
+
if (await isPortFree(port, host))
|
|
27
|
+
return port;
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`No free port found in ${start}–${start + limit - 1} on ${host}.`);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../../src/lib/ports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,mFAAmF;AACnF,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC;AAExC;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,IAAI,GAAW,YAAY;IAClE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,YAAY,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1C,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChE,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC;AAYD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EACjC,KAAK,EACL,IAAI,GAAG,YAAY,EACnB,KAAK,EACL,KAAK,GAAG,GAAG,GACK;IAChB,KAAK,IAAI,IAAI,GAAG,KAAK,EAAE,IAAI,GAAG,KAAK,GAAG,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;QACrE,IAAI,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAC/B,IAAI,MAAM,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IAChD,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,yBAAyB,KAAK,IAAI,KAAK,GAAG,KAAK,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,CAAC;AACrF,CAAC","sourcesContent":["import { createServer } from 'node:net';\n\n/** Loopback host every local mock and component binds to unless told otherwise. */\nexport const DEFAULT_HOST = '127.0.0.1';\n\n/**\n * Whether `port` can be bound on `host` right now, tested by actually binding it\n * (the only answer the OS will give honestly — scanning `lsof` output would miss\n * sockets owned by other users and races either way).\n */\nexport function isPortFree(port: number, host: string = DEFAULT_HOST): Promise<boolean> {\n return new Promise((resolve) => {\n const probe = createServer();\n probe.once('error', () => resolve(false));\n probe.once('listening', () => probe.close(() => resolve(true)));\n probe.listen(port, host);\n });\n}\n\nexport interface FreePortOptions {\n /** First port to try. */\n start: number;\n host?: string;\n /** Ports to treat as taken even if the OS says they are free (already handed out). */\n taken?: Set<number>;\n /** How many ports to try before giving up. */\n limit?: number;\n}\n\n/**\n * First bindable port at or after `start`, skipping any already handed out in\n * this run. Callers scan a small window — a machine with 200 consecutive busy\n * ports in this range has a bigger problem than port selection.\n */\nexport async function findFreePort({\n start,\n host = DEFAULT_HOST,\n taken,\n limit = 200,\n}: FreePortOptions): Promise<number> {\n for (let port = start; port < start + limit && port <= 65535; port++) {\n if (taken?.has(port)) continue;\n if (await isPortFree(port, host)) return port;\n }\n throw new Error(`No free port found in ${start}–${start + limit - 1} on ${host}.`);\n}\n"]}
|