@tetherto/mdk-cli 0.0.0-stage → 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 +446 -2
- 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 +57 -4
- 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
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { copyFileSync, cpSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { parse as parseYaml } from 'yaml';
|
|
6
|
+
import { fetchTemplate } from './fetch-template.js';
|
|
7
|
+
import { installDeps, npmBin } from './npm.js';
|
|
8
|
+
import { DIRS } from './project.js';
|
|
9
|
+
import { isInside, NAME_RE } from './names.js';
|
|
10
|
+
import { checkoutRoot } from './checkout.js';
|
|
11
|
+
import { pkg as cliPkg } from './pkg.js';
|
|
12
|
+
import { DASHBOARD_APPS_ENV } from './dashboard-apps.js';
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
/** Template subtree name — a runnable Vite app that doubles as the CLI template. */
|
|
15
|
+
const TEMPLATE_DIR_NAME = 'mdk-ui-shell-template';
|
|
16
|
+
/** Package root of the CLI (from `dist/lib/*.js` and `src/lib/*.ts` alike). */
|
|
17
|
+
const CLI_ROOT = resolve(__dirname, '..', '..');
|
|
18
|
+
/**
|
|
19
|
+
* Default ref for the GitHub template: this CLI's own release tag, so a given
|
|
20
|
+
* CLI version always scaffolds (and `npm install`s) the template it shipped
|
|
21
|
+
* with rather than whatever is on a mutable branch.
|
|
22
|
+
*/
|
|
23
|
+
export function defaultTemplateRef() {
|
|
24
|
+
return `v${cliPkg.version}`;
|
|
25
|
+
}
|
|
26
|
+
/** Branch, tag or SHA — the characters a git ref in a URL path can safely use. */
|
|
27
|
+
const REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
28
|
+
/**
|
|
29
|
+
* Builds the GitHub tree URL for the UI shell template at a given ref. Standalone
|
|
30
|
+
* (published) installs scaffold from here since the heavy Vite app is not bundled
|
|
31
|
+
* into the CLI package.
|
|
32
|
+
*/
|
|
33
|
+
export function shellTemplateUrl(ref = defaultTemplateRef()) {
|
|
34
|
+
if (!REF_RE.test(ref) || ref.includes('..'))
|
|
35
|
+
throw new Error(`Invalid git ref "${ref}".`);
|
|
36
|
+
return `https://github.com/tetherto/mdk/tree/${ref}/examples/${TEMPLATE_DIR_NAME}`;
|
|
37
|
+
}
|
|
38
|
+
/** Folder + package.json name for a stack's dashboard app. */
|
|
39
|
+
export function dashboardNameFromStack(stackName) {
|
|
40
|
+
return `${stackName}-dashboard`;
|
|
41
|
+
}
|
|
42
|
+
/** The MDK UI library packages the template links via `file:` (monorepo-local). */
|
|
43
|
+
const MDK_DEP_SCOPE = '@tetherto/';
|
|
44
|
+
/**
|
|
45
|
+
* Locates the runnable UI shell template inside the monorepo the CLI runs from
|
|
46
|
+
* (`<root>/packages/cli` → `<root>/examples/mdk-ui-shell-template`). Returns the
|
|
47
|
+
* template dir (and monorepo root) when the CLI is run from the monorepo, else
|
|
48
|
+
* null (published/standalone — scaffold from GitHub instead).
|
|
49
|
+
*/
|
|
50
|
+
function findLocalTemplate() {
|
|
51
|
+
const root = checkoutRoot(CLI_ROOT);
|
|
52
|
+
if (!root)
|
|
53
|
+
return null;
|
|
54
|
+
const templateDir = join(root, 'examples', TEMPLATE_DIR_NAME);
|
|
55
|
+
return existsSync(join(templateDir, 'package.json')) ? { templateDir, monorepoRoot: root } : null;
|
|
56
|
+
}
|
|
57
|
+
/** The published range to pin standalone MDK deps to (`^<cliVersion>`, else `latest`). */
|
|
58
|
+
function publishedMdkRange() {
|
|
59
|
+
try {
|
|
60
|
+
const pkg = JSON.parse(readFileSync(join(CLI_ROOT, 'package.json'), 'utf8'));
|
|
61
|
+
if (pkg.version)
|
|
62
|
+
return `^${pkg.version}`;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* fall through */
|
|
66
|
+
}
|
|
67
|
+
return 'latest';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Disk artifacts the runnable template accrues in place (it is a real app you can
|
|
71
|
+
* `npm run dev`) that must never land in a scaffolded copy.
|
|
72
|
+
*/
|
|
73
|
+
const COPY_EXCLUDES = new Set([
|
|
74
|
+
'node_modules',
|
|
75
|
+
'dist',
|
|
76
|
+
'coverage',
|
|
77
|
+
'.env',
|
|
78
|
+
'.env.local',
|
|
79
|
+
'.vite',
|
|
80
|
+
'package-lock.json',
|
|
81
|
+
]);
|
|
82
|
+
function isCopyExcluded(src) {
|
|
83
|
+
const base = src.split('/').pop() ?? '';
|
|
84
|
+
return COPY_EXCLUDES.has(base) || base.endsWith('.log') || base.endsWith('.tsbuildinfo');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Scaffolds the MDK UI shell (dashboard) into a project directory.
|
|
88
|
+
*
|
|
89
|
+
* When the CLI runs from the monorepo, it copies the local
|
|
90
|
+
* `examples/mdk-ui-shell-template` (no network). Otherwise it downloads the
|
|
91
|
+
* template subtree from GitHub. Either way it finalizes the copy for its target:
|
|
92
|
+
* sets the app name, rewrites the template's monorepo-local `file:` MDK deps
|
|
93
|
+
* (→ absolute local links in the monorepo, → a published range standalone),
|
|
94
|
+
* and injects the display name.
|
|
95
|
+
*/
|
|
96
|
+
export async function createDashboard(opts) {
|
|
97
|
+
if (!NAME_RE.test(opts.name)) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
message: `Invalid dashboard name "${opts.name}". Use lowercase letters, digits, '.', '_' or '-'.`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// The target is removed recursively under --force, so it must be a directory
|
|
104
|
+
// strictly inside `<parentDir>/apps` — never `apps` itself or anything above.
|
|
105
|
+
const appsRoot = resolve(opts.parentDir, DIRS.apps);
|
|
106
|
+
const appPath = resolve(opts.parentDir, opts.subdir ?? DIRS.dashboard);
|
|
107
|
+
if (appPath === appsRoot || !isInside(appsRoot, appPath)) {
|
|
108
|
+
return { ok: false, message: `Refusing to scaffold outside ${appsRoot}: ${appPath}` };
|
|
109
|
+
}
|
|
110
|
+
if (existsSync(appPath) && !opts.force) {
|
|
111
|
+
return { ok: false, message: `${appPath} already exists (use --force to overwrite).` };
|
|
112
|
+
}
|
|
113
|
+
const local = findLocalTemplate();
|
|
114
|
+
const gatewayUrl = opts.gatewayUrl ?? readGatewayUrl(opts.parentDir);
|
|
115
|
+
try {
|
|
116
|
+
if (local) {
|
|
117
|
+
rmSync(appPath, { recursive: true, force: true });
|
|
118
|
+
cpSync(local.templateDir, appPath, {
|
|
119
|
+
recursive: true,
|
|
120
|
+
filter: (src) => !isCopyExcluded(src),
|
|
121
|
+
});
|
|
122
|
+
finalizeShell(appPath, opts.name, { templateDir: local.templateDir, isMonorepo: true, gatewayUrl });
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
await fetchTemplate(shellTemplateUrl(opts.ref), appPath, opts.force ?? false);
|
|
126
|
+
finalizeShell(appPath, opts.name, { templateDir: appPath, isMonorepo: false, gatewayUrl });
|
|
127
|
+
}
|
|
128
|
+
// Installed in place, not at the project root: the dashboard is deliberately
|
|
129
|
+
// outside the npm workspaces (see project.ts WORKSPACES) so its Vite/React
|
|
130
|
+
// tree is never hoisted alongside the `file:`-linked MDK UI packages.
|
|
131
|
+
const install = installDeps(appPath, opts.install !== false);
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
message: '',
|
|
135
|
+
appPath,
|
|
136
|
+
source: local ? 'monorepo' : 'github',
|
|
137
|
+
installWarning: install.ok ? undefined : install.message,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
message: local
|
|
144
|
+
? 'Could not scaffold the UI shell from the local monorepo template.'
|
|
145
|
+
: 'Could not fetch the UI shell (check your network connection and the ref).',
|
|
146
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Finalize the freshly copied tree for the new runnable-template conventions:
|
|
152
|
+
* - remove a stale `package-lock.json`.
|
|
153
|
+
* - copy `.env.example` → `.env` so the app loads its config out of the box.
|
|
154
|
+
* - set package.json `name` + rewrite the template's `file:` MDK deps for the
|
|
155
|
+
* target context (absolute local links in the monorepo; a published range
|
|
156
|
+
* standalone) so `npm install` resolves them from the new location.
|
|
157
|
+
* - inject the chosen name into the `APP_NAME` display constant.
|
|
158
|
+
*/
|
|
159
|
+
function finalizeShell(dir, appName, { templateDir, isMonorepo, gatewayUrl, }) {
|
|
160
|
+
rmSync(join(dir, 'package-lock.json'), { force: true });
|
|
161
|
+
// Activate the env file: Vite loads `.env`, not `.env.example`. Copied rather
|
|
162
|
+
// than renamed so the example stays in the repo as the committed reference —
|
|
163
|
+
// the generated `.env` is machine-local and gitignored.
|
|
164
|
+
const envExample = join(dir, '.env.example');
|
|
165
|
+
const envFile = join(dir, '.env');
|
|
166
|
+
if (existsSync(envExample) && !existsSync(envFile)) {
|
|
167
|
+
copyFileSync(envExample, envFile);
|
|
168
|
+
}
|
|
169
|
+
// Point the dev-server proxy + OAuth at this stack's gateway (mdk.yaml port).
|
|
170
|
+
if (gatewayUrl && existsSync(envFile)) {
|
|
171
|
+
let env = readFileSync(envFile, 'utf8');
|
|
172
|
+
env = setEnvVar(env, 'VITE_GATEWAY_URL', gatewayUrl);
|
|
173
|
+
env = setEnvVar(env, 'VITE_OAUTH_BASE_URL', gatewayUrl);
|
|
174
|
+
writeFileSync(envFile, env, 'utf8');
|
|
175
|
+
}
|
|
176
|
+
rewritePackageJson(join(dir, 'package.json'), { appName, templateDir, isMonorepo });
|
|
177
|
+
setDisplayName(dir, appName);
|
|
178
|
+
}
|
|
179
|
+
/** Reads `http://localhost:<gateway.port>` from `<parentDir>/mdk.yaml`, else null. */
|
|
180
|
+
function readGatewayUrl(parentDir) {
|
|
181
|
+
const specPath = resolve(parentDir, 'mdk.yaml');
|
|
182
|
+
if (!existsSync(specPath))
|
|
183
|
+
return null;
|
|
184
|
+
try {
|
|
185
|
+
const doc = parseYaml(readFileSync(specPath, 'utf8'));
|
|
186
|
+
const port = doc.spec?.gateway?.port;
|
|
187
|
+
if (typeof port === 'number' && port > 0)
|
|
188
|
+
return `http://localhost:${port}`;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* not fatal — fall back to the template default */
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
/** Sets `KEY=value` in a dotenv string, replacing an existing line or appending. */
|
|
196
|
+
function setEnvVar(content, key, value) {
|
|
197
|
+
const line = `${key}=${value}`;
|
|
198
|
+
const re = new RegExp(`^${key}=.*$`, 'm');
|
|
199
|
+
if (re.test(content))
|
|
200
|
+
return content.replace(re, line);
|
|
201
|
+
return `${content}${content.endsWith('\n') ? '' : '\n'}${line}\n`;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Rewrite the scaffolded package.json for its target context. The app takes the
|
|
205
|
+
* chosen bare name (the `@tetherto/` scope belongs to the MDK library packages).
|
|
206
|
+
* The template links MDK packages via `file:` so it runs in place; a copy must
|
|
207
|
+
* not inherit those now-broken relative links:
|
|
208
|
+
* - MONOREPO → resolve each `file:` link against the template dir to an
|
|
209
|
+
* absolute local path (works from any scaffold location, no workspace needed).
|
|
210
|
+
* - STANDALONE → rewrite `file:` MDK deps to a published range.
|
|
211
|
+
*/
|
|
212
|
+
function rewritePackageJson(pkgJsonPath, { appName, templateDir, isMonorepo }) {
|
|
213
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
214
|
+
pkg.name = appName;
|
|
215
|
+
const deps = pkg.dependencies;
|
|
216
|
+
if (deps) {
|
|
217
|
+
const range = publishedMdkRange();
|
|
218
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
219
|
+
if (!spec.startsWith('file:') || !name.startsWith(MDK_DEP_SCOPE))
|
|
220
|
+
continue;
|
|
221
|
+
deps[name] = isMonorepo
|
|
222
|
+
? `file:${resolve(templateDir, spec.slice('file:'.length))}`
|
|
223
|
+
: range;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
writeFileSync(pkgJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The shell centralises its human-facing name in an `APP_NAME` constant
|
|
230
|
+
* (src/constants/env.ts) — the browser tab + Home heading read it. Rewrite it to
|
|
231
|
+
* the chosen name. No-op for templates without that constant.
|
|
232
|
+
*/
|
|
233
|
+
function setDisplayName(dir, appName) {
|
|
234
|
+
const envPath = join(dir, 'src', 'constants', 'env.ts');
|
|
235
|
+
if (!existsSync(envPath))
|
|
236
|
+
return;
|
|
237
|
+
const content = readFileSync(envPath, 'utf8');
|
|
238
|
+
// A replacer function (not a `$`-pattern string) and a JSON-encoded literal,
|
|
239
|
+
// so nothing in the name is interpreted by `replace` or as TypeScript.
|
|
240
|
+
const next = content.replace(/(export const APP_NAME\s*=\s*)(['"]).*?\2/, (_match, prefix) => `${prefix}${JSON.stringify(appName)}`);
|
|
241
|
+
if (next !== content)
|
|
242
|
+
writeFileSync(envPath, next, 'utf8');
|
|
243
|
+
}
|
|
244
|
+
/** True when `dir` has a package.json declaring an npm `dev` script. */
|
|
245
|
+
function hasDevScript(dir) {
|
|
246
|
+
try {
|
|
247
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
248
|
+
return typeof pkg.scripts?.dev === 'string';
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Locates a scaffolded dashboard app in a project. Tries the canonical
|
|
256
|
+
* `apps/dashboard` first (what `mdk create dashboard` uses with no `--name`);
|
|
257
|
+
* failing that, scans `apps/*` for the first app with a `dev` script, so a
|
|
258
|
+
* dashboard created with a custom name is still found without extra plumbing.
|
|
259
|
+
* Returns null when no app is found.
|
|
260
|
+
*/
|
|
261
|
+
export function findDashboardDir(projectDir) {
|
|
262
|
+
const canonical = resolve(projectDir, DIRS.dashboard);
|
|
263
|
+
if (hasDevScript(canonical))
|
|
264
|
+
return canonical;
|
|
265
|
+
const appsRoot = resolve(projectDir, DIRS.apps);
|
|
266
|
+
if (!existsSync(appsRoot))
|
|
267
|
+
return null;
|
|
268
|
+
for (const name of readdirSync(appsRoot).sort()) {
|
|
269
|
+
const dir = join(appsRoot, name);
|
|
270
|
+
if (hasDevScript(dir))
|
|
271
|
+
return dir;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Runs the dashboard's dev server (`npm run dev`) in `dir`, streaming its
|
|
277
|
+
* output directly to this process's stdio.
|
|
278
|
+
*
|
|
279
|
+
* Unlike the Kernel, Gateway and Workers — all booted in-process — the
|
|
280
|
+
* dashboard is a separate Vite/React app with its own dependency tree (kept out
|
|
281
|
+
* of the project's npm workspaces precisely so its React is never hoisted
|
|
282
|
+
* alongside anyone else's, see `project.ts`), so it can only run as its own
|
|
283
|
+
* process. `apps` (see `describeDashboardApps`) reaches it in the
|
|
284
|
+
* `MDK_DASHBOARD_APPS` environment variable, so it can load their UIs. The
|
|
285
|
+
* returned promise resolves once the process has actually spawned
|
|
286
|
+
* (Node's `'spawn'` event) and rejects if it never could (e.g. `npm` missing).
|
|
287
|
+
*/
|
|
288
|
+
export function runDashboard(dir, apps = []) {
|
|
289
|
+
return new Promise((resolveHandle, reject) => {
|
|
290
|
+
// Fixed argv only: win32 needs a shell to run `npm.cmd` (see npm.ts).
|
|
291
|
+
const child = spawn(npmBin(), ['run', 'dev'], {
|
|
292
|
+
cwd: dir,
|
|
293
|
+
stdio: 'inherit',
|
|
294
|
+
// The installed apps travel to the dev server, which serves their UIs.
|
|
295
|
+
env: { ...process.env, [DASHBOARD_APPS_ENV]: JSON.stringify(apps) },
|
|
296
|
+
shell: process.platform === 'win32',
|
|
297
|
+
});
|
|
298
|
+
child.once('error', (error) => {
|
|
299
|
+
reject(error.code === 'ENOENT'
|
|
300
|
+
? new Error('npm was not found on PATH — cannot start the dashboard dev server.')
|
|
301
|
+
: new Error(`Failed to start the dashboard dev server: ${error.message}`));
|
|
302
|
+
});
|
|
303
|
+
child.once('spawn', () => {
|
|
304
|
+
resolveHandle({
|
|
305
|
+
process: child,
|
|
306
|
+
stop: () => new Promise((resolveStop) => {
|
|
307
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
308
|
+
resolveStop();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
child.once('exit', () => resolveStop());
|
|
312
|
+
child.kill('SIGTERM');
|
|
313
|
+
}),
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=dashboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../../src/lib/dashboard.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcess } from 'node:child_process';\nimport { copyFileSync, cpSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { parse as parseYaml } from 'yaml';\nimport { fetchTemplate } from './fetch-template.js';\nimport { installDeps, npmBin } from './npm.js';\nimport { DIRS } from './project.js';\nimport { isInside, NAME_RE } from './names.js';\nimport { checkoutRoot } from './checkout.js';\nimport { pkg as cliPkg } from './pkg.js';\nimport { DASHBOARD_APPS_ENV, type DashboardApp } from './dashboard-apps.js';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/** Template subtree name — a runnable Vite app that doubles as the CLI template. */\nconst TEMPLATE_DIR_NAME = 'mdk-ui-shell-template';\n\n/** Package root of the CLI (from `dist/lib/*.js` and `src/lib/*.ts` alike). */\nconst CLI_ROOT = resolve(__dirname, '..', '..');\n\n/**\n * Default ref for the GitHub template: this CLI's own release tag, so a given\n * CLI version always scaffolds (and `npm install`s) the template it shipped\n * with rather than whatever is on a mutable branch.\n */\nexport function defaultTemplateRef(): string {\n return `v${cliPkg.version}`;\n}\n\n/** Branch, tag or SHA — the characters a git ref in a URL path can safely use. */\nconst REF_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;\n\n/**\n * Builds the GitHub tree URL for the UI shell template at a given ref. Standalone\n * (published) installs scaffold from here since the heavy Vite app is not bundled\n * into the CLI package.\n */\nexport function shellTemplateUrl(ref = defaultTemplateRef()): string {\n if (!REF_RE.test(ref) || ref.includes('..')) throw new Error(`Invalid git ref \"${ref}\".`);\n return `https://github.com/tetherto/mdk/tree/${ref}/examples/${TEMPLATE_DIR_NAME}`;\n}\n\n/** Folder + package.json name for a stack's dashboard app. */\nexport function dashboardNameFromStack(stackName: string): string {\n return `${stackName}-dashboard`;\n}\n\nexport interface DashboardOptions {\n /** Package + display name for the generated shell (e.g. `my-stack-dashboard`). */\n name: string;\n /** Project root the app is created under. */\n parentDir: string;\n /**\n * Where the app lands, relative to `parentDir` (default `apps/dashboard`).\n * Kept separate from `name` so the path stays stable while the package keeps a\n * descriptive, stack-derived name.\n */\n subdir?: string;\n /** Branch/tag to fetch from GitHub when not inside the monorepo (default: `v<cli version>`). */\n ref?: string;\n /** Overwrite the target directory if it already exists (default: false). */\n force?: boolean;\n /** Run `npm install` in the scaffolded dashboard (default: true). */\n install?: boolean;\n /**\n * Gateway base URL the dev server proxies to (`http://localhost:<port>`).\n * When omitted it is read from `<parentDir>/mdk.yaml`'s `spec.gateway.port`.\n */\n gatewayUrl?: string;\n}\n\nexport interface DashboardResult {\n ok: boolean;\n message: string;\n detail?: string;\n appPath?: string;\n /** Where the template came from: the local monorepo checkout or GitHub. */\n source?: 'monorepo' | 'github';\n /** Warning surfaced when `npm install` could not complete (scaffold still ok). */\n installWarning?: string;\n}\n\n/** The MDK UI library packages the template links via `file:` (monorepo-local). */\nconst MDK_DEP_SCOPE = '@tetherto/';\n\n/**\n * Locates the runnable UI shell template inside the monorepo the CLI runs from\n * (`<root>/packages/cli` → `<root>/examples/mdk-ui-shell-template`). Returns the\n * template dir (and monorepo root) when the CLI is run from the monorepo, else\n * null (published/standalone — scaffold from GitHub instead).\n */\nfunction findLocalTemplate(): { templateDir: string; monorepoRoot: string } | null {\n const root = checkoutRoot(CLI_ROOT);\n if (!root) return null;\n const templateDir = join(root, 'examples', TEMPLATE_DIR_NAME);\n return existsSync(join(templateDir, 'package.json')) ? { templateDir, monorepoRoot: root } : null;\n}\n\n/** The published range to pin standalone MDK deps to (`^<cliVersion>`, else `latest`). */\nfunction publishedMdkRange(): string {\n try {\n const pkg = JSON.parse(readFileSync(join(CLI_ROOT, 'package.json'), 'utf8')) as {\n version?: string;\n };\n if (pkg.version) return `^${pkg.version}`;\n } catch {\n /* fall through */\n }\n return 'latest';\n}\n\n/**\n * Disk artifacts the runnable template accrues in place (it is a real app you can\n * `npm run dev`) that must never land in a scaffolded copy.\n */\nconst COPY_EXCLUDES = new Set([\n 'node_modules',\n 'dist',\n 'coverage',\n '.env',\n '.env.local',\n '.vite',\n 'package-lock.json',\n]);\n\nfunction isCopyExcluded(src: string): boolean {\n const base = src.split('/').pop() ?? '';\n return COPY_EXCLUDES.has(base) || base.endsWith('.log') || base.endsWith('.tsbuildinfo');\n}\n\n/**\n * Scaffolds the MDK UI shell (dashboard) into a project directory.\n *\n * When the CLI runs from the monorepo, it copies the local\n * `examples/mdk-ui-shell-template` (no network). Otherwise it downloads the\n * template subtree from GitHub. Either way it finalizes the copy for its target:\n * sets the app name, rewrites the template's monorepo-local `file:` MDK deps\n * (→ absolute local links in the monorepo, → a published range standalone),\n * and injects the display name.\n */\nexport async function createDashboard(opts: DashboardOptions): Promise<DashboardResult> {\n if (!NAME_RE.test(opts.name)) {\n return {\n ok: false,\n message: `Invalid dashboard name \"${opts.name}\". Use lowercase letters, digits, '.', '_' or '-'.`,\n };\n }\n // The target is removed recursively under --force, so it must be a directory\n // strictly inside `<parentDir>/apps` — never `apps` itself or anything above.\n const appsRoot = resolve(opts.parentDir, DIRS.apps);\n const appPath = resolve(opts.parentDir, opts.subdir ?? DIRS.dashboard);\n if (appPath === appsRoot || !isInside(appsRoot, appPath)) {\n return { ok: false, message: `Refusing to scaffold outside ${appsRoot}: ${appPath}` };\n }\n if (existsSync(appPath) && !opts.force) {\n return { ok: false, message: `${appPath} already exists (use --force to overwrite).` };\n }\n\n const local = findLocalTemplate();\n const gatewayUrl = opts.gatewayUrl ?? readGatewayUrl(opts.parentDir);\n\n try {\n if (local) {\n rmSync(appPath, { recursive: true, force: true });\n cpSync(local.templateDir, appPath, {\n recursive: true,\n filter: (src) => !isCopyExcluded(src),\n });\n finalizeShell(appPath, opts.name, { templateDir: local.templateDir, isMonorepo: true, gatewayUrl });\n } else {\n await fetchTemplate(shellTemplateUrl(opts.ref), appPath, opts.force ?? false);\n finalizeShell(appPath, opts.name, { templateDir: appPath, isMonorepo: false, gatewayUrl });\n }\n\n // Installed in place, not at the project root: the dashboard is deliberately\n // outside the npm workspaces (see project.ts WORKSPACES) so its Vite/React\n // tree is never hoisted alongside the `file:`-linked MDK UI packages.\n const install = installDeps(appPath, opts.install !== false);\n return {\n ok: true,\n message: '',\n appPath,\n source: local ? 'monorepo' : 'github',\n installWarning: install.ok ? undefined : install.message,\n };\n } catch (error) {\n return {\n ok: false,\n message: local\n ? 'Could not scaffold the UI shell from the local monorepo template.'\n : 'Could not fetch the UI shell (check your network connection and the ref).',\n detail: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\n/**\n * Finalize the freshly copied tree for the new runnable-template conventions:\n * - remove a stale `package-lock.json`.\n * - copy `.env.example` → `.env` so the app loads its config out of the box.\n * - set package.json `name` + rewrite the template's `file:` MDK deps for the\n * target context (absolute local links in the monorepo; a published range\n * standalone) so `npm install` resolves them from the new location.\n * - inject the chosen name into the `APP_NAME` display constant.\n */\nfunction finalizeShell(\n dir: string,\n appName: string,\n {\n templateDir,\n isMonorepo,\n gatewayUrl,\n }: { templateDir: string; isMonorepo: boolean; gatewayUrl?: string | null },\n): void {\n rmSync(join(dir, 'package-lock.json'), { force: true });\n\n // Activate the env file: Vite loads `.env`, not `.env.example`. Copied rather\n // than renamed so the example stays in the repo as the committed reference —\n // the generated `.env` is machine-local and gitignored.\n const envExample = join(dir, '.env.example');\n const envFile = join(dir, '.env');\n if (existsSync(envExample) && !existsSync(envFile)) {\n copyFileSync(envExample, envFile);\n }\n\n // Point the dev-server proxy + OAuth at this stack's gateway (mdk.yaml port).\n if (gatewayUrl && existsSync(envFile)) {\n let env = readFileSync(envFile, 'utf8');\n env = setEnvVar(env, 'VITE_GATEWAY_URL', gatewayUrl);\n env = setEnvVar(env, 'VITE_OAUTH_BASE_URL', gatewayUrl);\n writeFileSync(envFile, env, 'utf8');\n }\n\n rewritePackageJson(join(dir, 'package.json'), { appName, templateDir, isMonorepo });\n setDisplayName(dir, appName);\n}\n\n/** Reads `http://localhost:<gateway.port>` from `<parentDir>/mdk.yaml`, else null. */\nfunction readGatewayUrl(parentDir: string): string | null {\n const specPath = resolve(parentDir, 'mdk.yaml');\n if (!existsSync(specPath)) return null;\n try {\n const doc = parseYaml(readFileSync(specPath, 'utf8')) as {\n spec?: { gateway?: { port?: number } };\n };\n const port = doc.spec?.gateway?.port;\n if (typeof port === 'number' && port > 0) return `http://localhost:${port}`;\n } catch {\n /* not fatal — fall back to the template default */\n }\n return null;\n}\n\n/** Sets `KEY=value` in a dotenv string, replacing an existing line or appending. */\nfunction setEnvVar(content: string, key: string, value: string): string {\n const line = `${key}=${value}`;\n const re = new RegExp(`^${key}=.*$`, 'm');\n if (re.test(content)) return content.replace(re, line);\n return `${content}${content.endsWith('\\n') ? '' : '\\n'}${line}\\n`;\n}\n\n/**\n * Rewrite the scaffolded package.json for its target context. The app takes the\n * chosen bare name (the `@tetherto/` scope belongs to the MDK library packages).\n * The template links MDK packages via `file:` so it runs in place; a copy must\n * not inherit those now-broken relative links:\n * - MONOREPO → resolve each `file:` link against the template dir to an\n * absolute local path (works from any scaffold location, no workspace needed).\n * - STANDALONE → rewrite `file:` MDK deps to a published range.\n */\nfunction rewritePackageJson(\n pkgJsonPath: string,\n { appName, templateDir, isMonorepo }: { appName: string; templateDir: string; isMonorepo: boolean },\n): void {\n const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')) as {\n name?: string;\n dependencies?: Record<string, string>;\n };\n pkg.name = appName;\n\n const deps = pkg.dependencies;\n if (deps) {\n const range = publishedMdkRange();\n for (const [name, spec] of Object.entries(deps)) {\n if (!spec.startsWith('file:') || !name.startsWith(MDK_DEP_SCOPE)) continue;\n deps[name] = isMonorepo\n ? `file:${resolve(templateDir, spec.slice('file:'.length))}`\n : range;\n }\n }\n writeFileSync(pkgJsonPath, `${JSON.stringify(pkg, null, 2)}\\n`, 'utf8');\n}\n\n/**\n * The shell centralises its human-facing name in an `APP_NAME` constant\n * (src/constants/env.ts) — the browser tab + Home heading read it. Rewrite it to\n * the chosen name. No-op for templates without that constant.\n */\nfunction setDisplayName(dir: string, appName: string): void {\n const envPath = join(dir, 'src', 'constants', 'env.ts');\n if (!existsSync(envPath)) return;\n const content = readFileSync(envPath, 'utf8');\n // A replacer function (not a `$`-pattern string) and a JSON-encoded literal,\n // so nothing in the name is interpreted by `replace` or as TypeScript.\n const next = content.replace(\n /(export const APP_NAME\\s*=\\s*)(['\"]).*?\\2/,\n (_match, prefix: string) => `${prefix}${JSON.stringify(appName)}`,\n );\n if (next !== content) writeFileSync(envPath, next, 'utf8');\n}\n\n/** True when `dir` has a package.json declaring an npm `dev` script. */\nfunction hasDevScript(dir: string): boolean {\n try {\n const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as {\n scripts?: Record<string, string>;\n };\n return typeof pkg.scripts?.dev === 'string';\n } catch {\n return false;\n }\n}\n\n/**\n * Locates a scaffolded dashboard app in a project. Tries the canonical\n * `apps/dashboard` first (what `mdk create dashboard` uses with no `--name`);\n * failing that, scans `apps/*` for the first app with a `dev` script, so a\n * dashboard created with a custom name is still found without extra plumbing.\n * Returns null when no app is found.\n */\nexport function findDashboardDir(projectDir: string): string | null {\n const canonical = resolve(projectDir, DIRS.dashboard);\n if (hasDevScript(canonical)) return canonical;\n\n const appsRoot = resolve(projectDir, DIRS.apps);\n if (!existsSync(appsRoot)) return null;\n for (const name of readdirSync(appsRoot).sort()) {\n const dir = join(appsRoot, name);\n if (hasDevScript(dir)) return dir;\n }\n return null;\n}\n\n/** A running dashboard dev server and how to stop it. */\nexport interface DashboardHandle {\n process: ChildProcess;\n stop: () => Promise<void>;\n}\n\n/**\n * Runs the dashboard's dev server (`npm run dev`) in `dir`, streaming its\n * output directly to this process's stdio.\n *\n * Unlike the Kernel, Gateway and Workers — all booted in-process — the\n * dashboard is a separate Vite/React app with its own dependency tree (kept out\n * of the project's npm workspaces precisely so its React is never hoisted\n * alongside anyone else's, see `project.ts`), so it can only run as its own\n * process. `apps` (see `describeDashboardApps`) reaches it in the\n * `MDK_DASHBOARD_APPS` environment variable, so it can load their UIs. The\n * returned promise resolves once the process has actually spawned\n * (Node's `'spawn'` event) and rejects if it never could (e.g. `npm` missing).\n */\nexport function runDashboard(dir: string, apps: DashboardApp[] = []): Promise<DashboardHandle> {\n return new Promise((resolveHandle, reject) => {\n // Fixed argv only: win32 needs a shell to run `npm.cmd` (see npm.ts).\n const child = spawn(npmBin(), ['run', 'dev'], {\n cwd: dir,\n stdio: 'inherit',\n // The installed apps travel to the dev server, which serves their UIs.\n env: { ...process.env, [DASHBOARD_APPS_ENV]: JSON.stringify(apps) },\n shell: process.platform === 'win32',\n });\n\n child.once('error', (error: NodeJS.ErrnoException) => {\n reject(\n error.code === 'ENOENT'\n ? new Error('npm was not found on PATH — cannot start the dashboard dev server.')\n : new Error(`Failed to start the dashboard dev server: ${error.message}`),\n );\n });\n\n child.once('spawn', () => {\n resolveHandle({\n process: child,\n stop: () =>\n new Promise((resolveStop) => {\n if (child.exitCode !== null || child.signalCode !== null) {\n resolveStop();\n return;\n }\n child.once('exit', () => resolveStop());\n child.kill('SIGTERM');\n }),\n });\n });\n });\n}\n"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/** Lowest Node major the CLI and the MDK runtime packages support (their `engines`). */
|
|
4
|
+
export const NODE_MIN_MAJOR = 24;
|
|
5
|
+
/**
|
|
6
|
+
* Lightweight, generic environment detection for the onboarding wizard.
|
|
7
|
+
* Nothing here is MDK-specific — it only inspects the current working directory
|
|
8
|
+
* and the running Node process.
|
|
9
|
+
*/
|
|
10
|
+
export function detectEnvironment(cwd = process.cwd()) {
|
|
11
|
+
const nodeVersion = process.versions.node;
|
|
12
|
+
const major = Number(nodeVersion.split('.')[0] ?? '0');
|
|
13
|
+
let packageManager = 'npm';
|
|
14
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml')))
|
|
15
|
+
packageManager = 'pnpm';
|
|
16
|
+
else if (existsSync(join(cwd, 'yarn.lock')))
|
|
17
|
+
packageManager = 'yarn';
|
|
18
|
+
else if (existsSync(join(cwd, 'bun.lockb')))
|
|
19
|
+
packageManager = 'bun';
|
|
20
|
+
let agentClient = 'none';
|
|
21
|
+
if (existsSync(join(cwd, '.cursor')))
|
|
22
|
+
agentClient = 'cursor';
|
|
23
|
+
else if (existsSync(join(cwd, '.claude')))
|
|
24
|
+
agentClient = 'claude';
|
|
25
|
+
else if (existsSync(join(cwd, '.codex')))
|
|
26
|
+
agentClient = 'codex';
|
|
27
|
+
else if (existsSync(join(cwd, '.cline')))
|
|
28
|
+
agentClient = 'cline';
|
|
29
|
+
return {
|
|
30
|
+
nodeVersion,
|
|
31
|
+
nodeOk: major >= NODE_MIN_MAJOR,
|
|
32
|
+
packageManager,
|
|
33
|
+
git: existsSync(join(cwd, '.git')),
|
|
34
|
+
existingSpec: existsSync(join(cwd, 'mdk.yaml')),
|
|
35
|
+
agentClient,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=detect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect.js","sourceRoot":"","sources":["../../src/lib/detect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,wFAAwF;AACxF,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAWjC;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAG,GAAW,OAAO,CAAC,GAAG,EAAE;IAC3D,MAAM,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;IAEvD,IAAI,cAAc,GAAG,KAAK,CAAC;IAC3B,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;QAAE,cAAc,GAAG,MAAM,CAAC;SAChE,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAAE,cAAc,GAAG,MAAM,CAAC;SAChE,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAAE,cAAc,GAAG,KAAK,CAAC;IAEpE,IAAI,WAAW,GAAG,MAAM,CAAC;IACzB,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAAE,WAAW,GAAG,QAAQ,CAAC;SACxD,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAAE,WAAW,GAAG,QAAQ,CAAC;SAC7D,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAAE,WAAW,GAAG,OAAO,CAAC;SAC3D,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAAE,WAAW,GAAG,OAAO,CAAC;IAEhE,OAAO;QACL,WAAW;QACX,MAAM,EAAE,KAAK,IAAI,cAAc;QAC/B,cAAc;QACd,GAAG,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAClC,YAAY,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAC/C,WAAW;KACZ,CAAC;AACJ,CAAC","sourcesContent":["import { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** Lowest Node major the CLI and the MDK runtime packages support (their `engines`). */\nexport const NODE_MIN_MAJOR = 24;\n\nexport interface Environment {\n nodeVersion: string;\n nodeOk: boolean;\n packageManager: string;\n git: boolean;\n existingSpec: boolean;\n agentClient: string;\n}\n\n/**\n * Lightweight, generic environment detection for the onboarding wizard.\n * Nothing here is MDK-specific — it only inspects the current working directory\n * and the running Node process.\n */\nexport function detectEnvironment(cwd: string = process.cwd()): Environment {\n const nodeVersion = process.versions.node;\n const major = Number(nodeVersion.split('.')[0] ?? '0');\n\n let packageManager = 'npm';\n if (existsSync(join(cwd, 'pnpm-lock.yaml'))) packageManager = 'pnpm';\n else if (existsSync(join(cwd, 'yarn.lock'))) packageManager = 'yarn';\n else if (existsSync(join(cwd, 'bun.lockb'))) packageManager = 'bun';\n\n let agentClient = 'none';\n if (existsSync(join(cwd, '.cursor'))) agentClient = 'cursor';\n else if (existsSync(join(cwd, '.claude'))) agentClient = 'claude';\n else if (existsSync(join(cwd, '.codex'))) agentClient = 'codex';\n else if (existsSync(join(cwd, '.cline'))) agentClient = 'cline';\n\n return {\n nodeVersion,\n nodeOk: major >= NODE_MIN_MAJOR,\n packageManager,\n git: existsSync(join(cwd, '.git')),\n existingSpec: existsSync(join(cwd, 'mdk.yaml')),\n agentClient,\n };\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parseEnv } from 'node:util';
|
|
4
|
+
/**
|
|
5
|
+
* The project's machine-local secrets file. `mdk.yaml` is meant to be committed,
|
|
6
|
+
* so secret setup answers live here and the spec only holds `${NAME}`
|
|
7
|
+
* placeholders (resolved by `loadStackSpec`). Always gitignored — see
|
|
8
|
+
* `ensureProjectGitignore`.
|
|
9
|
+
*/
|
|
10
|
+
export const ENV_FILE = '.env';
|
|
11
|
+
const ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
12
|
+
/** Parses `<projectDir>/.env`; `{}` when it is absent or unreadable. */
|
|
13
|
+
export function readProjectEnv(projectDir) {
|
|
14
|
+
const file = join(projectDir, ENV_FILE);
|
|
15
|
+
if (!existsSync(file))
|
|
16
|
+
return {};
|
|
17
|
+
try {
|
|
18
|
+
return parseEnv(readFileSync(file, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Quotes a value so Node's dotenv parser reads it back verbatim: single quotes
|
|
26
|
+
* and backticks are literal, double quotes are a last resort (they expand `\n`).
|
|
27
|
+
*/
|
|
28
|
+
function quote(value) {
|
|
29
|
+
if (/[\r\n]/.test(value))
|
|
30
|
+
throw new Error('a .env value cannot contain a line break');
|
|
31
|
+
for (const q of ["'", '`', '"']) {
|
|
32
|
+
if (!value.includes(q) && !(q === '"' && value.includes('\\n')))
|
|
33
|
+
return `${q}${value}${q}`;
|
|
34
|
+
}
|
|
35
|
+
throw new Error('a .env value cannot contain all of \', ` and "');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Sets `NAME=value` lines in `<projectDir>/.env`, replacing an existing line for
|
|
39
|
+
* the same name or appending. The file is owner-only (0600).
|
|
40
|
+
*/
|
|
41
|
+
export function writeProjectEnv(projectDir, vars) {
|
|
42
|
+
const file = join(projectDir, ENV_FILE);
|
|
43
|
+
let content = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
44
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
45
|
+
if (!ENV_NAME_RE.test(name))
|
|
46
|
+
throw new Error(`invalid environment variable name "${name}"`);
|
|
47
|
+
const line = `${name}=${quote(value)}`;
|
|
48
|
+
const re = new RegExp(`^${name}=.*$`, 'm');
|
|
49
|
+
content = re.test(content)
|
|
50
|
+
? content.replace(re, () => line)
|
|
51
|
+
: `${content}${content && !content.endsWith('\n') ? '\n' : ''}${line}\n`;
|
|
52
|
+
}
|
|
53
|
+
writeFileSync(file, content, { encoding: 'utf8', mode: 0o600 });
|
|
54
|
+
chmodSync(file, 0o600);
|
|
55
|
+
return file;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=env-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-file.js","sourceRoot":"","sources":["../../src/lib/env-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC;AAE/B,MAAM,WAAW,GAAG,0BAA0B,CAAC;AAE/C,wEAAwE;AACxE,MAAM,UAAU,cAAc,CAAC,UAAkB;IAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IACxC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAA2B,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,KAAK,CAAC,KAAa;IAC1B,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IACtF,KAAK,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;QAChC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC;IAC7F,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;AACpE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,UAAkB,EAAE,IAA4B;IAC9E,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IACxC,IAAI,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACjD,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,IAAI,GAAG,CAAC,CAAC;QAC5F,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACvC,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,EAAE,GAAG,CAAC,CAAC;QAC3C,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;YACxB,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC;YACjC,CAAC,CAAC,GAAG,OAAO,GAAG,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,IAAI,CAAC;IAC7E,CAAC;IACD,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAChE,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACvB,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { parseEnv } from 'node:util';\n\n/**\n * The project's machine-local secrets file. `mdk.yaml` is meant to be committed,\n * so secret setup answers live here and the spec only holds `${NAME}`\n * placeholders (resolved by `loadStackSpec`). Always gitignored — see\n * `ensureProjectGitignore`.\n */\nexport const ENV_FILE = '.env';\n\nconst ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/** Parses `<projectDir>/.env`; `{}` when it is absent or unreadable. */\nexport function readProjectEnv(projectDir: string): Record<string, string> {\n const file = join(projectDir, ENV_FILE);\n if (!existsSync(file)) return {};\n try {\n return parseEnv(readFileSync(file, 'utf8')) as Record<string, string>;\n } catch {\n return {};\n }\n}\n\n/**\n * Quotes a value so Node's dotenv parser reads it back verbatim: single quotes\n * and backticks are literal, double quotes are a last resort (they expand `\\n`).\n */\nfunction quote(value: string): string {\n if (/[\\r\\n]/.test(value)) throw new Error('a .env value cannot contain a line break');\n for (const q of [\"'\", '`', '\"']) {\n if (!value.includes(q) && !(q === '\"' && value.includes('\\\\n'))) return `${q}${value}${q}`;\n }\n throw new Error('a .env value cannot contain all of \\', ` and \"');\n}\n\n/**\n * Sets `NAME=value` lines in `<projectDir>/.env`, replacing an existing line for\n * the same name or appending. The file is owner-only (0600).\n */\nexport function writeProjectEnv(projectDir: string, vars: Record<string, string>): string {\n const file = join(projectDir, ENV_FILE);\n let content = existsSync(file) ? readFileSync(file, 'utf8') : '';\n for (const [name, value] of Object.entries(vars)) {\n if (!ENV_NAME_RE.test(name)) throw new Error(`invalid environment variable name \"${name}\"`);\n const line = `${name}=${quote(value)}`;\n const re = new RegExp(`^${name}=.*$`, 'm');\n content = re.test(content)\n ? content.replace(re, () => line)\n : `${content}${content && !content.endsWith('\\n') ? '\\n' : ''}${line}\\n`;\n }\n writeFileSync(file, content, { encoding: 'utf8', mode: 0o600 });\n chmodSync(file, 0o600);\n return file;\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import degit from 'degit';
|
|
2
|
+
/**
|
|
3
|
+
* Converts a GitHub "tree" URL into a degit source spec.
|
|
4
|
+
*
|
|
5
|
+
* `https://github.com/<owner>/<repo>/tree/<ref>/<path...>`
|
|
6
|
+
* → `<owner>/<repo>/<path...>#<ref>`
|
|
7
|
+
*
|
|
8
|
+
* Note: refs containing slashes are not supported by this parser (single-segment
|
|
9
|
+
* refs like `main` or `v1.2.3` only). That is sufficient for our pinned URLs.
|
|
10
|
+
*/
|
|
11
|
+
export function toDegitSrc(treeUrl) {
|
|
12
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+?)\/?$/.exec(treeUrl);
|
|
13
|
+
if (!match) {
|
|
14
|
+
throw new Error(`Not a GitHub tree URL: ${treeUrl}`);
|
|
15
|
+
}
|
|
16
|
+
const [, owner, repo, ref, path] = match;
|
|
17
|
+
return `${owner}/${repo}/${path}#${ref}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Downloads a single subdirectory of a GitHub repo into `dest` using the same
|
|
21
|
+
* "download tarball, extract one subtree" technique as `create-next-app`.
|
|
22
|
+
*
|
|
23
|
+
* Requires network access to resolve the ref's commit hash (via `git ls-remote`).
|
|
24
|
+
* `cache` is intentionally left off: with it enabled degit only reads a local
|
|
25
|
+
* `map.json` and never resolves the hash online, so a cold cache always fails.
|
|
26
|
+
* With it off, degit still reuses a previously downloaded `<hash>.tar.gz` under
|
|
27
|
+
* `~/.degit`, so repeat runs of the same commit skip the download.
|
|
28
|
+
*/
|
|
29
|
+
export async function fetchTemplate(treeUrl, dest, force = false) {
|
|
30
|
+
const emitter = degit(toDegitSrc(treeUrl), { cache: false, force, verbose: false });
|
|
31
|
+
await emitter.clone(dest);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=fetch-template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-template.js","sourceRoot":"","sources":["../../src/lib/fetch-template.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,MAAM,KAAK,GAAG,qEAAqE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAClG,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,0BAA0B,OAAO,EAAE,CAAC,CAAC;IACvD,CAAC;IACD,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC;IACzC,OAAO,GAAG,KAAK,IAAI,IAAI,IAAI,IAAI,IAAI,GAAG,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAe,EAAE,IAAY,EAAE,KAAK,GAAG,KAAK;IAC9E,MAAM,OAAO,GAAG,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACpF,MAAM,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC","sourcesContent":["import degit from 'degit';\n\n/**\n * Converts a GitHub \"tree\" URL into a degit source spec.\n *\n * `https://github.com/<owner>/<repo>/tree/<ref>/<path...>`\n * → `<owner>/<repo>/<path...>#<ref>`\n *\n * Note: refs containing slashes are not supported by this parser (single-segment\n * refs like `main` or `v1.2.3` only). That is sufficient for our pinned URLs.\n */\nexport function toDegitSrc(treeUrl: string): string {\n const match = /^https?:\\/\\/github\\.com\\/([^/]+)\\/([^/]+)\\/tree\\/([^/]+)\\/(.+?)\\/?$/.exec(treeUrl);\n if (!match) {\n throw new Error(`Not a GitHub tree URL: ${treeUrl}`);\n }\n const [, owner, repo, ref, path] = match;\n return `${owner}/${repo}/${path}#${ref}`;\n}\n\n/**\n * Downloads a single subdirectory of a GitHub repo into `dest` using the same\n * \"download tarball, extract one subtree\" technique as `create-next-app`.\n *\n * Requires network access to resolve the ref's commit hash (via `git ls-remote`).\n * `cache` is intentionally left off: with it enabled degit only reads a local\n * `map.json` and never resolves the hash online, so a cold cache always fails.\n * With it off, degit still reuses a previously downloaded `<hash>.tar.gz` under\n * `~/.degit`, so repeat runs of the same commit skip the download.\n */\nexport async function fetchTemplate(treeUrl: string, dest: string, force = false): Promise<void> {\n const emitter = degit(toDegitSrc(treeUrl), { cache: false, force, verbose: false });\n await emitter.clone(dest);\n}\n"]}
|
package/dist/lib/help.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Help } from 'commander';
|
|
2
|
+
/** An `Examples:` block for `addHelpText('after', ...)`. */
|
|
3
|
+
export function examples(...lines) {
|
|
4
|
+
return `\nExamples:\n${lines.map((line) => ` $ ${line}`).join('\n')}\n`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Help settings every command inherits, so set on the root before any subcommand
|
|
8
|
+
* is added: consistent wording, and no `(default: false)` on plain flags.
|
|
9
|
+
*/
|
|
10
|
+
export function configureHelp(program) {
|
|
11
|
+
return program
|
|
12
|
+
.helpOption('-h, --help', 'Show help')
|
|
13
|
+
.helpCommand('help [command]', 'Show help for a command')
|
|
14
|
+
.configureHelp({
|
|
15
|
+
optionDescription(option) {
|
|
16
|
+
if (option.isBoolean() && option.defaultValue === false)
|
|
17
|
+
return option.description;
|
|
18
|
+
return Help.prototype.optionDescription.call(this, option);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=help.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"help.js","sourceRoot":"","sources":["../../src/lib/help.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAA6B,MAAM,WAAW,CAAC;AAE5D,4DAA4D;AAC5D,MAAM,UAAU,QAAQ,CAAC,GAAG,KAAe;IACzC,OAAO,gBAAgB,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AAC3E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,OAAO,OAAO;SACX,UAAU,CAAC,YAAY,EAAE,WAAW,CAAC;SACrC,WAAW,CAAC,gBAAgB,EAAE,yBAAyB,CAAC;SACxD,aAAa,CAAC;QACb,iBAAiB,CAAa,MAAc;YAC1C,IAAI,MAAM,CAAC,SAAS,EAAE,IAAI,MAAM,CAAC,YAAY,KAAK,KAAK;gBAAE,OAAO,MAAM,CAAC,WAAW,CAAC;YACnF,OAAO,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7D,CAAC;KACF,CAAC,CAAC;AACP,CAAC","sourcesContent":["import { Help, type Command, type Option } from 'commander';\n\n/** An `Examples:` block for `addHelpText('after', ...)`. */\nexport function examples(...lines: string[]): string {\n return `\\nExamples:\\n${lines.map((line) => ` $ ${line}`).join('\\n')}\\n`;\n}\n\n/**\n * Help settings every command inherits, so set on the root before any subcommand\n * is added: consistent wording, and no `(default: false)` on plain flags.\n */\nexport function configureHelp(program: Command): Command {\n return program\n .helpOption('-h, --help', 'Show help')\n .helpCommand('help [command]', 'Show help for a command')\n .configureHelp({\n optionDescription(this: Help, option: Option): string {\n if (option.isBoolean() && option.defaultValue === false) return option.description;\n return Help.prototype.optionDescription.call(this, option);\n },\n });\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Worker, plugin, app, dashboard and stack names. Each one becomes a single path
|
|
4
|
+
* segment and (part of) an npm package name, so it must not contain `/`, `\`,
|
|
5
|
+
* start with `.`, or carry anything a shell or source file would interpret.
|
|
6
|
+
*/
|
|
7
|
+
export const NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
8
|
+
/** npm scope for `--org`, checked after an optional leading `@` is stripped. */
|
|
9
|
+
export const ORG_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
10
|
+
export function isValidName(name) {
|
|
11
|
+
return NAME_RE.test(name);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Normalises a `--org` value: strips one leading `@` and validates the rest.
|
|
15
|
+
* An absent or empty value means "no scope".
|
|
16
|
+
*/
|
|
17
|
+
export function parseOrg(org) {
|
|
18
|
+
if (org == null)
|
|
19
|
+
return { ok: true };
|
|
20
|
+
const bare = org.trim().replace(/^@/, '');
|
|
21
|
+
if (!bare)
|
|
22
|
+
return { ok: true };
|
|
23
|
+
if (!ORG_RE.test(bare)) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
message: `Invalid --org "${org}". Use lowercase letters, digits, '.', '_' or '-' (an optional leading '@' is allowed).`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, org: bare };
|
|
30
|
+
}
|
|
31
|
+
/** True when `child` resolves to `parent` itself or a path strictly beneath it. */
|
|
32
|
+
export function isInside(parent, child) {
|
|
33
|
+
const rel = relative(resolve(parent), resolve(child));
|
|
34
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=names.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"names.js","sourceRoot":"","sources":["../../src/lib/names.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAE/D;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,wBAAwB,CAAC;AAEhD,gFAAgF;AAChF,MAAM,CAAC,MAAM,MAAM,GAAG,wBAAwB,CAAC;AAE/C,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC;AAID;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAuB;IAC9C,IAAI,GAAG,IAAI,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACrC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAC1C,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,kBAAkB,GAAG,yFAAyF;SACxH,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;AACjC,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,QAAQ,CAAC,MAAc,EAAE,KAAa;IACpD,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IACtD,OAAO,GAAG,KAAK,EAAE,IAAI,CAAC,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AACzF,CAAC","sourcesContent":["import { isAbsolute, relative, resolve, sep } from 'node:path';\n\n/**\n * Worker, plugin, app, dashboard and stack names. Each one becomes a single path\n * segment and (part of) an npm package name, so it must not contain `/`, `\\`,\n * start with `.`, or carry anything a shell or source file would interpret.\n */\nexport const NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;\n\n/** npm scope for `--org`, checked after an optional leading `@` is stripped. */\nexport const ORG_RE = /^[a-z0-9][a-z0-9._-]*$/;\n\nexport function isValidName(name: string): boolean {\n return NAME_RE.test(name);\n}\n\nexport type OrgResult = { ok: true; org?: string } | { ok: false; message: string };\n\n/**\n * Normalises a `--org` value: strips one leading `@` and validates the rest.\n * An absent or empty value means \"no scope\".\n */\nexport function parseOrg(org: string | undefined): OrgResult {\n if (org == null) return { ok: true };\n const bare = org.trim().replace(/^@/, '');\n if (!bare) return { ok: true };\n if (!ORG_RE.test(bare)) {\n return {\n ok: false,\n message: `Invalid --org \"${org}\". Use lowercase letters, digits, '.', '_' or '-' (an optional leading '@' is allowed).`,\n };\n }\n return { ok: true, org: bare };\n}\n\n/** True when `child` resolves to `parent` itself or a path strictly beneath it. */\nexport function isInside(parent: string, child: string): boolean {\n const rel = relative(resolve(parent), resolve(child));\n return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));\n}\n"]}
|