@motionscript/cli 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/LICENSE +201 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +78 -0
- package/dist/cli.js.map +1 -0
- package/dist/registry/add.d.ts +8 -0
- package/dist/registry/add.d.ts.map +1 -0
- package/dist/registry/add.js +93 -0
- package/dist/registry/add.js.map +1 -0
- package/dist/registry/config.d.ts +28 -0
- package/dist/registry/config.d.ts.map +1 -0
- package/dist/registry/config.js +55 -0
- package/dist/registry/config.js.map +1 -0
- package/dist/registry/diff.d.ts +16 -0
- package/dist/registry/diff.d.ts.map +1 -0
- package/dist/registry/diff.js +83 -0
- package/dist/registry/diff.js.map +1 -0
- package/dist/registry/fetch.d.ts +16 -0
- package/dist/registry/fetch.d.ts.map +1 -0
- package/dist/registry/fetch.js +59 -0
- package/dist/registry/fetch.js.map +1 -0
- package/dist/registry/init.d.ts +15 -0
- package/dist/registry/init.d.ts.map +1 -0
- package/dist/registry/init.js +69 -0
- package/dist/registry/init.js.map +1 -0
- package/dist/registry/install.d.ts +37 -0
- package/dist/registry/install.d.ts.map +1 -0
- package/dist/registry/install.js +105 -0
- package/dist/registry/install.js.map +1 -0
- package/dist/registry/list.d.ts +13 -0
- package/dist/registry/list.d.ts.map +1 -0
- package/dist/registry/list.js +61 -0
- package/dist/registry/list.js.map +1 -0
- package/dist/registry/lock.d.ts +73 -0
- package/dist/registry/lock.d.ts.map +1 -0
- package/dist/registry/lock.js +104 -0
- package/dist/registry/lock.js.map +1 -0
- package/dist/registry/package-manager.d.ts +28 -0
- package/dist/registry/package-manager.d.ts.map +1 -0
- package/dist/registry/package-manager.js +61 -0
- package/dist/registry/package-manager.js.map +1 -0
- package/dist/registry/protocol.d.ts +135 -0
- package/dist/registry/protocol.d.ts.map +1 -0
- package/dist/registry/protocol.js +88 -0
- package/dist/registry/protocol.js.map +1 -0
- package/package.json +64 -3
- package/README.md +0 -4
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* What was installed, from where, at what version — and whether it has been
|
|
6
|
+
* edited since.
|
|
7
|
+
*
|
|
8
|
+
* A copied component is source you are *meant* to change: that is the whole
|
|
9
|
+
* point of copying it rather than depending on it. But then "is this file
|
|
10
|
+
* yours or ours" stops having an obvious answer, and every later question —
|
|
11
|
+
* can `ms add` overwrite this, what did upstream change, which of these
|
|
12
|
+
* components am I even carrying — depends on it.
|
|
13
|
+
*
|
|
14
|
+
* So each install records the hash of the bytes it wrote. Re-hashing the file
|
|
15
|
+
* on disk answers "modified since install" exactly, with no heuristic.
|
|
16
|
+
*
|
|
17
|
+
* ## Why a separate file
|
|
18
|
+
*
|
|
19
|
+
* `motionscript.json` is hand-authored configuration: registries, paths, an
|
|
20
|
+
* alias. This is derived, rewritten on every install, and of no interest to a
|
|
21
|
+
* human until something is wrong. Mixing the two would mean `ms add` rewriting
|
|
22
|
+
* a file the user edits, and a merge conflict spanning both kinds of content.
|
|
23
|
+
* Every package manager makes the same split for the same reason.
|
|
24
|
+
*
|
|
25
|
+
* ## Why not a header in each file
|
|
26
|
+
*
|
|
27
|
+
* A "generated, do not edit" banner above a file whose purpose is to be edited
|
|
28
|
+
* is a lie, and the first `format-on-save` moves or mangles it. The record
|
|
29
|
+
* belongs beside the project, not inside the thing it describes.
|
|
30
|
+
*/
|
|
31
|
+
export const LOCK_FILENAME = 'motionscript.lock.json';
|
|
32
|
+
/** What the lock file used to be called, oldest first. Both still read, never written. */
|
|
33
|
+
export const LEGACY_LOCK_FILENAMES = ['motion-script.lock.json', 'components.lock.json'];
|
|
34
|
+
export function hashContent(content) {
|
|
35
|
+
return `sha256-${createHash('sha256').update(content, 'utf8').digest('base64')}`;
|
|
36
|
+
}
|
|
37
|
+
export function emptyLock() {
|
|
38
|
+
return {
|
|
39
|
+
$schema: 'https://motionscript.dev/schema/motionscript.lock.json',
|
|
40
|
+
lockfileVersion: 1,
|
|
41
|
+
components: {},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function loadLock(projectRoot) {
|
|
45
|
+
const lockPath = [LOCK_FILENAME, ...LEGACY_LOCK_FILENAMES]
|
|
46
|
+
.map((name) => path.join(projectRoot, name))
|
|
47
|
+
.find((candidate) => fs.existsSync(candidate));
|
|
48
|
+
if (!lockPath)
|
|
49
|
+
return emptyLock();
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
52
|
+
// A lock from a future version describes installs whose shape this
|
|
53
|
+
// client does not know. Starting over loses provenance, which is a
|
|
54
|
+
// nuisance; acting on a record we cannot read would overwrite files
|
|
55
|
+
// whose state we have guessed at, which is not.
|
|
56
|
+
if (parsed.lockfileVersion !== 1) {
|
|
57
|
+
throw new Error(`${LOCK_FILENAME} is version ${parsed.lockfileVersion}, which this CLI does not understand. Upgrade @motionscript/cli.`);
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error instanceof SyntaxError) {
|
|
63
|
+
throw new Error(`${LOCK_FILENAME} is not valid JSON. Delete it to start over.`);
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function writeLock(projectRoot, lock) {
|
|
69
|
+
// Components sorted, so two installs in different orders produce the same
|
|
70
|
+
// file and a diff shows only what actually changed.
|
|
71
|
+
const sorted = {
|
|
72
|
+
...lock,
|
|
73
|
+
components: Object.fromEntries(Object.entries(lock.components).sort(([a], [b]) => a.localeCompare(b))),
|
|
74
|
+
};
|
|
75
|
+
fs.writeFileSync(path.join(projectRoot, LOCK_FILENAME), `${JSON.stringify(sorted, null, 2)}\n`);
|
|
76
|
+
}
|
|
77
|
+
/** Records one item's install, replacing any previous record of it. */
|
|
78
|
+
export function recordInstall(lock, requestedName, item, url, written) {
|
|
79
|
+
lock.components[requestedName] = {
|
|
80
|
+
registry: requestedName.startsWith('@') ? requestedName.split('/')[0] : 'default',
|
|
81
|
+
url,
|
|
82
|
+
item: item.name,
|
|
83
|
+
...(item.version ? { version: item.version } : {}),
|
|
84
|
+
...(item.kit ? { kit: item.kit } : {}),
|
|
85
|
+
installedAt: new Date().toISOString(),
|
|
86
|
+
files: Object.fromEntries(Object.entries(written).map(([declared, { target, content }]) => [
|
|
87
|
+
declared,
|
|
88
|
+
{ target, installed: hashContent(content) },
|
|
89
|
+
])),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function fileState(projectRoot, file) {
|
|
93
|
+
const full = path.join(projectRoot, file.target);
|
|
94
|
+
if (!fs.existsSync(full))
|
|
95
|
+
return 'missing';
|
|
96
|
+
return hashContent(fs.readFileSync(full, 'utf8')) === file.installed ? 'unchanged' : 'modified';
|
|
97
|
+
}
|
|
98
|
+
/** Every file of a component that is no longer as it was installed. */
|
|
99
|
+
export function changedFiles(projectRoot, entry) {
|
|
100
|
+
return Object.entries(entry.files)
|
|
101
|
+
.filter(([, file]) => fileState(projectRoot, file) !== 'unchanged')
|
|
102
|
+
.map(([declared]) => declared);
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/registry/lock.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport type { RegistryItem } from './protocol.js';\n\n/**\n * What was installed, from where, at what version — and whether it has been\n * edited since.\n *\n * A copied component is source you are *meant* to change: that is the whole\n * point of copying it rather than depending on it. But then \"is this file\n * yours or ours\" stops having an obvious answer, and every later question —\n * can `ms add` overwrite this, what did upstream change, which of these\n * components am I even carrying — depends on it.\n *\n * So each install records the hash of the bytes it wrote. Re-hashing the file\n * on disk answers \"modified since install\" exactly, with no heuristic.\n *\n * ## Why a separate file\n *\n * `motionscript.json` is hand-authored configuration: registries, paths, an\n * alias. This is derived, rewritten on every install, and of no interest to a\n * human until something is wrong. Mixing the two would mean `ms add` rewriting\n * a file the user edits, and a merge conflict spanning both kinds of content.\n * Every package manager makes the same split for the same reason.\n *\n * ## Why not a header in each file\n *\n * A \"generated, do not edit\" banner above a file whose purpose is to be edited\n * is a lie, and the first `format-on-save` moves or mangles it. The record\n * belongs beside the project, not inside the thing it describes.\n */\n\nexport const LOCK_FILENAME = 'motionscript.lock.json';\n\n/** What the lock file used to be called, oldest first. Both still read, never written. */\nexport const LEGACY_LOCK_FILENAMES = ['motion-script.lock.json', 'components.lock.json'];\n\n/** One file as it was written. */\nexport interface LockedFile {\n /** Where it landed, relative to the project root. */\n target: string;\n /** sha256 of the bytes written, after any import rewriting. */\n installed: string;\n}\n\n/** One installed component. */\nexport interface LockedComponent {\n /** `\"default\"`, or the `@namespace` it came from. */\n registry: string;\n /** The exact URL it was read from. */\n url: string;\n /** The item name, with any namespace stripped. */\n item: string;\n /** The item's version at install time, when it declared one. */\n version?: string;\n /** The kit its copied source imports, and which stays a dependency. */\n kit?: string;\n installedAt: string;\n files: Record<string, LockedFile>;\n}\n\nexport interface ComponentsLock {\n $schema?: string;\n lockfileVersion: 1;\n components: Record<string, LockedComponent>;\n}\n\nexport function hashContent(content: string): string {\n return `sha256-${createHash('sha256').update(content, 'utf8').digest('base64')}`;\n}\n\nexport function emptyLock(): ComponentsLock {\n return {\n $schema: 'https://motionscript.dev/schema/motionscript.lock.json',\n lockfileVersion: 1,\n components: {},\n };\n}\n\nexport function loadLock(projectRoot: string): ComponentsLock {\n const lockPath = [LOCK_FILENAME, ...LEGACY_LOCK_FILENAMES]\n .map((name) => path.join(projectRoot, name))\n .find((candidate) => fs.existsSync(candidate));\n if (!lockPath) return emptyLock();\n try {\n const parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8')) as ComponentsLock;\n // A lock from a future version describes installs whose shape this\n // client does not know. Starting over loses provenance, which is a\n // nuisance; acting on a record we cannot read would overwrite files\n // whose state we have guessed at, which is not.\n if (parsed.lockfileVersion !== 1) {\n throw new Error(\n `${LOCK_FILENAME} is version ${parsed.lockfileVersion}, which this CLI does not understand. Upgrade @motionscript/cli.`,\n );\n }\n return parsed;\n } catch (error) {\n if (error instanceof SyntaxError) {\n throw new Error(`${LOCK_FILENAME} is not valid JSON. Delete it to start over.`);\n }\n throw error;\n }\n}\n\nexport function writeLock(projectRoot: string, lock: ComponentsLock): void {\n // Components sorted, so two installs in different orders produce the same\n // file and a diff shows only what actually changed.\n const sorted: ComponentsLock = {\n ...lock,\n components: Object.fromEntries(\n Object.entries(lock.components).sort(([a], [b]) => a.localeCompare(b)),\n ),\n };\n fs.writeFileSync(path.join(projectRoot, LOCK_FILENAME), `${JSON.stringify(sorted, null, 2)}\\n`);\n}\n\n/** Records one item's install, replacing any previous record of it. */\nexport function recordInstall(\n lock: ComponentsLock,\n requestedName: string,\n item: RegistryItem,\n url: string,\n written: Record<string, { target: string; content: string }>,\n): void {\n lock.components[requestedName] = {\n registry: requestedName.startsWith('@') ? requestedName.split('/')[0] : 'default',\n url,\n item: item.name,\n ...(item.version ? { version: item.version } : {}),\n ...(item.kit ? { kit: item.kit } : {}),\n installedAt: new Date().toISOString(),\n files: Object.fromEntries(\n Object.entries(written).map(([declared, { target, content }]) => [\n declared,\n { target, installed: hashContent(content) },\n ]),\n ),\n };\n}\n\n/** How one installed file stands against what was written. */\nexport type FileState = 'unchanged' | 'modified' | 'missing';\n\nexport function fileState(projectRoot: string, file: LockedFile): FileState {\n const full = path.join(projectRoot, file.target);\n if (!fs.existsSync(full)) return 'missing';\n return hashContent(fs.readFileSync(full, 'utf8')) === file.installed ? 'unchanged' : 'modified';\n}\n\n/** Every file of a component that is no longer as it was installed. */\nexport function changedFiles(projectRoot: string, entry: LockedComponent): string[] {\n return Object.entries(entry.files)\n .filter(([, file]) => fileState(projectRoot, file) !== 'unchanged')\n .map(([declared]) => declared);\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm';
|
|
2
|
+
/**
|
|
3
|
+
* Detect the project's package manager. Prefers a lockfile in `projectRoot`
|
|
4
|
+
* itself — deliberately *not* walking up to parent directories: an unrelated
|
|
5
|
+
* lockfile higher in the tree (a stray one in the user's home directory, an
|
|
6
|
+
* unconnected project) would misdetect. That means a monorepo whose lockfile
|
|
7
|
+
* lives above the individual project root falls through to
|
|
8
|
+
* `npm_config_user_agent` instead — which is set correctly for that case,
|
|
9
|
+
* since a monorepo install/exec is normally run through the package manager
|
|
10
|
+
* itself. Only the direct-invocation case (`ms add` typed straight into a
|
|
11
|
+
* plain project, no wrapper setting that env var) relies on the lockfile
|
|
12
|
+
* check. Final fallback is `npm`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectPackageManager(projectRoot: string): PackageManager;
|
|
15
|
+
/** Merge `dependencies` (pkg -> range) into the project's package.json `dependencies`. */
|
|
16
|
+
export declare function mergePackageJsonDependencies(projectRoot: string, dependencies: Record<string, string>): void;
|
|
17
|
+
/**
|
|
18
|
+
* Merge `dependencies` into package.json, then run the package manager's
|
|
19
|
+
* install. `shell: true` is required on Windows — npm/pnpm/yarn ship as
|
|
20
|
+
* `.cmd` shims, not directly-spawnable binaries. Passed as a single command
|
|
21
|
+
* string (no separate `args` array) rather than `spawnSync(pm, ['install'],
|
|
22
|
+
* { shell: true })`: combining an args array with `shell: true` trips
|
|
23
|
+
* Node's DEP0190 warning (args aren't escaped, only concatenated) on every
|
|
24
|
+
* run. `pm` is one of the four literal {@link PackageManager} values, never
|
|
25
|
+
* user input, so there's nothing to escape here regardless.
|
|
26
|
+
*/
|
|
27
|
+
export declare function installDependencies(pm: PackageManager, dependencies: Record<string, string>, projectRoot: string): void;
|
|
28
|
+
//# sourceMappingURL=package-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-manager.d.ts","sourceRoot":"","sources":["../../src/registry/package-manager.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;AAU7D;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CASxE;AAED,0FAA0F;AAC1F,wBAAgB,4BAA4B,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAU5G;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAQvH"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
const LOCKFILES = {
|
|
5
|
+
'pnpm-lock.yaml': 'pnpm',
|
|
6
|
+
'yarn.lock': 'yarn',
|
|
7
|
+
'bun.lockb': 'bun',
|
|
8
|
+
'bun.lock': 'bun',
|
|
9
|
+
'package-lock.json': 'npm',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Detect the project's package manager. Prefers a lockfile in `projectRoot`
|
|
13
|
+
* itself — deliberately *not* walking up to parent directories: an unrelated
|
|
14
|
+
* lockfile higher in the tree (a stray one in the user's home directory, an
|
|
15
|
+
* unconnected project) would misdetect. That means a monorepo whose lockfile
|
|
16
|
+
* lives above the individual project root falls through to
|
|
17
|
+
* `npm_config_user_agent` instead — which is set correctly for that case,
|
|
18
|
+
* since a monorepo install/exec is normally run through the package manager
|
|
19
|
+
* itself. Only the direct-invocation case (`ms add` typed straight into a
|
|
20
|
+
* plain project, no wrapper setting that env var) relies on the lockfile
|
|
21
|
+
* check. Final fallback is `npm`.
|
|
22
|
+
*/
|
|
23
|
+
export function detectPackageManager(projectRoot) {
|
|
24
|
+
for (const [file, pm] of Object.entries(LOCKFILES)) {
|
|
25
|
+
if (fs.existsSync(path.join(projectRoot, file)))
|
|
26
|
+
return pm;
|
|
27
|
+
}
|
|
28
|
+
const fromUa = process.env.npm_config_user_agent?.split(' ')[0]?.split('/')[0];
|
|
29
|
+
if (fromUa === 'pnpm' || fromUa === 'yarn' || fromUa === 'bun' || fromUa === 'npm')
|
|
30
|
+
return fromUa;
|
|
31
|
+
return 'npm';
|
|
32
|
+
}
|
|
33
|
+
/** Merge `dependencies` (pkg -> range) into the project's package.json `dependencies`. */
|
|
34
|
+
export function mergePackageJsonDependencies(projectRoot, dependencies) {
|
|
35
|
+
if (Object.keys(dependencies).length === 0)
|
|
36
|
+
return;
|
|
37
|
+
const pkgJsonPath = path.join(projectRoot, 'package.json');
|
|
38
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
39
|
+
pkgJson.dependencies = { ...pkgJson.dependencies, ...dependencies };
|
|
40
|
+
fs.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Merge `dependencies` into package.json, then run the package manager's
|
|
44
|
+
* install. `shell: true` is required on Windows — npm/pnpm/yarn ship as
|
|
45
|
+
* `.cmd` shims, not directly-spawnable binaries. Passed as a single command
|
|
46
|
+
* string (no separate `args` array) rather than `spawnSync(pm, ['install'],
|
|
47
|
+
* { shell: true })`: combining an args array with `shell: true` trips
|
|
48
|
+
* Node's DEP0190 warning (args aren't escaped, only concatenated) on every
|
|
49
|
+
* run. `pm` is one of the four literal {@link PackageManager} values, never
|
|
50
|
+
* user input, so there's nothing to escape here regardless.
|
|
51
|
+
*/
|
|
52
|
+
export function installDependencies(pm, dependencies, projectRoot) {
|
|
53
|
+
if (Object.keys(dependencies).length === 0)
|
|
54
|
+
return;
|
|
55
|
+
mergePackageJsonDependencies(projectRoot, dependencies);
|
|
56
|
+
const result = spawnSync(`${pm} install`, { cwd: projectRoot, stdio: 'inherit', shell: true });
|
|
57
|
+
if (result.status !== 0) {
|
|
58
|
+
throw new Error(`"${pm} install" failed (exit code ${result.status ?? 'unknown'}).`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=package-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-manager.js","sourceRoot":"","sources":["../../src/registry/package-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAI/C,MAAM,SAAS,GAAmC;IAC9C,gBAAgB,EAAE,MAAM;IACxB,WAAW,EAAE,MAAM;IACnB,WAAW,EAAE,KAAK;IAClB,UAAU,EAAE,KAAK;IACjB,mBAAmB,EAAE,KAAK;CAC7B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAAC,WAAmB;IACpD,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACjD,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YAAE,OAAO,EAAE,CAAC;IAC/D,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,MAAM,CAAC;IAElG,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,4BAA4B,CAAC,WAAmB,EAAE,YAAoC;IAClG,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAEnD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,CAAC;IAC3D,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAG9D,CAAC;IACF,OAAO,CAAC,YAAY,GAAG,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,YAAY,EAAE,CAAC;IACpE,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAkB,EAAE,YAAoC,EAAE,WAAmB;IAC7G,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACnD,4BAA4B,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IAExD,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/F,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,IAAI,EAAE,+BAA+B,MAAM,CAAC,MAAM,IAAI,SAAS,IAAI,CAAC,CAAC;IACzF,CAAC;AACL,CAAC","sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { spawnSync } from 'node:child_process';\n\nexport type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm';\n\nconst LOCKFILES: Record<string, PackageManager> = {\n 'pnpm-lock.yaml': 'pnpm',\n 'yarn.lock': 'yarn',\n 'bun.lockb': 'bun',\n 'bun.lock': 'bun',\n 'package-lock.json': 'npm',\n};\n\n/**\n * Detect the project's package manager. Prefers a lockfile in `projectRoot`\n * itself — deliberately *not* walking up to parent directories: an unrelated\n * lockfile higher in the tree (a stray one in the user's home directory, an\n * unconnected project) would misdetect. That means a monorepo whose lockfile\n * lives above the individual project root falls through to\n * `npm_config_user_agent` instead — which is set correctly for that case,\n * since a monorepo install/exec is normally run through the package manager\n * itself. Only the direct-invocation case (`ms add` typed straight into a\n * plain project, no wrapper setting that env var) relies on the lockfile\n * check. Final fallback is `npm`.\n */\nexport function detectPackageManager(projectRoot: string): PackageManager {\n for (const [file, pm] of Object.entries(LOCKFILES)) {\n if (fs.existsSync(path.join(projectRoot, file))) return pm;\n }\n\n const fromUa = process.env.npm_config_user_agent?.split(' ')[0]?.split('/')[0];\n if (fromUa === 'pnpm' || fromUa === 'yarn' || fromUa === 'bun' || fromUa === 'npm') return fromUa;\n\n return 'npm';\n}\n\n/** Merge `dependencies` (pkg -> range) into the project's package.json `dependencies`. */\nexport function mergePackageJsonDependencies(projectRoot: string, dependencies: Record<string, string>): void {\n if (Object.keys(dependencies).length === 0) return;\n\n const pkgJsonPath = path.join(projectRoot, 'package.json');\n const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')) as {\n dependencies?: Record<string, string>;\n [key: string]: unknown;\n };\n pkgJson.dependencies = { ...pkgJson.dependencies, ...dependencies };\n fs.writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\\n`);\n}\n\n/**\n * Merge `dependencies` into package.json, then run the package manager's\n * install. `shell: true` is required on Windows — npm/pnpm/yarn ship as\n * `.cmd` shims, not directly-spawnable binaries. Passed as a single command\n * string (no separate `args` array) rather than `spawnSync(pm, ['install'],\n * { shell: true })`: combining an args array with `shell: true` trips\n * Node's DEP0190 warning (args aren't escaped, only concatenated) on every\n * run. `pm` is one of the four literal {@link PackageManager} values, never\n * user input, so there's nothing to escape here regardless.\n */\nexport function installDependencies(pm: PackageManager, dependencies: Record<string, string>, projectRoot: string): void {\n if (Object.keys(dependencies).length === 0) return;\n mergePackageJsonDependencies(projectRoot, dependencies);\n\n const result = spawnSync(`${pm} install`, { cwd: projectRoot, stdio: 'inherit', shell: true });\n if (result.status !== 0) {\n throw new Error(`\"${pm} install\" failed (exit code ${result.status ?? 'unknown'}).`);\n }\n}\n"]}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry protocol: how a component's source travels from wherever it is
|
|
3
|
+
* published to whoever installs it.
|
|
4
|
+
*
|
|
5
|
+
* Modelled on shadcn/ui's, because it is the one people already know and
|
|
6
|
+
* because it is deliberately dull — two static JSON endpoints, servable by
|
|
7
|
+
* anything that serves a file:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* GET <registry>/registry.json → the index: [{ name, type, description }]
|
|
11
|
+
* GET <registry>/<name>.json → one item: its files, deps and manifest
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Kept separate from `fetch.ts`'s `fs`/`fetch` transport because this half is
|
|
15
|
+
* pure — resolving a name to a URL involves no I/O — which is what makes it
|
|
16
|
+
* unit-testable with no network, and what would let a future non-CLI client
|
|
17
|
+
* (an in-browser editor) reuse the exact same resolution rules without
|
|
18
|
+
* depending on the whole CLI.
|
|
19
|
+
*/
|
|
20
|
+
export type RegistryItemType = "registry:component" | "registry:kit";
|
|
21
|
+
/** One row of a registry's index. */
|
|
22
|
+
export interface RegistryIndexEntry {
|
|
23
|
+
name: string;
|
|
24
|
+
type: RegistryItemType;
|
|
25
|
+
description?: string;
|
|
26
|
+
/** The version this item was cut from. */
|
|
27
|
+
version?: string;
|
|
28
|
+
/** Whether its source may be copied at all. */
|
|
29
|
+
forkable?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** One file of an item, with its content inlined. */
|
|
32
|
+
export interface RegistryFile {
|
|
33
|
+
/** Path relative to the components root, e.g. `"line-chart/chart-line.ts"`. */
|
|
34
|
+
path: string;
|
|
35
|
+
content: string;
|
|
36
|
+
}
|
|
37
|
+
/** A single installable component. */
|
|
38
|
+
export interface RegistryItem {
|
|
39
|
+
name: string;
|
|
40
|
+
type: RegistryItemType;
|
|
41
|
+
description?: string;
|
|
42
|
+
/** The version this was cut from, so a lock can record what it installed. */
|
|
43
|
+
version?: string;
|
|
44
|
+
/** npm dependencies, as `"pkg@range"`. */
|
|
45
|
+
dependencies?: string[];
|
|
46
|
+
/** Other item names, installed first. */
|
|
47
|
+
registryDependencies?: string[];
|
|
48
|
+
files: RegistryFile[];
|
|
49
|
+
/**
|
|
50
|
+
* The kit these files import and must **not** copy, as `"pkg@range"`.
|
|
51
|
+
*
|
|
52
|
+
* Always also present in {@link dependencies}; named separately because a
|
|
53
|
+
* host that copies source has to know which bare specifier must resolve to
|
|
54
|
+
* its own single copy rather than to a second one it installed alongside.
|
|
55
|
+
*/
|
|
56
|
+
kit?: string;
|
|
57
|
+
forkable?: boolean;
|
|
58
|
+
/** sha256 over the canonicalised file list, so a copy can be verified. */
|
|
59
|
+
integrity?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Whether a host with no package manager can install this: true only when
|
|
62
|
+
* every entry of {@link dependencies} is something it already bundles.
|
|
63
|
+
*
|
|
64
|
+
* An editor installs *source*, never npm. Declaring this lets it say "needs
|
|
65
|
+
* `@acme/foo`, which cannot be installed here" while the item is still a card
|
|
66
|
+
* in a list, rather than failing when the code is linked.
|
|
67
|
+
*/
|
|
68
|
+
standalone?: boolean;
|
|
69
|
+
}
|
|
70
|
+
/** A configured registry: a URL template, plus whatever it needs to be read. */
|
|
71
|
+
export interface RegistryConfig {
|
|
72
|
+
/** Contains a literal `{name}` placeholder. */
|
|
73
|
+
url: string;
|
|
74
|
+
headers?: Record<string, string>;
|
|
75
|
+
params?: Record<string, string>;
|
|
76
|
+
}
|
|
77
|
+
/** The `motionscript.json` a project keeps at its root. */
|
|
78
|
+
export interface ComponentsConfig {
|
|
79
|
+
$schema?: string;
|
|
80
|
+
project: string;
|
|
81
|
+
aliases: {
|
|
82
|
+
components: string;
|
|
83
|
+
};
|
|
84
|
+
paths: {
|
|
85
|
+
components: string;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Additional registries, keyed by `"@namespace"`.
|
|
89
|
+
*
|
|
90
|
+
* The default registry is never configured here — it is the client's own
|
|
91
|
+
* constant — matching shadcn's `components.json`, whose `registries` keys are
|
|
92
|
+
* schema-constrained to `^@`.
|
|
93
|
+
*/
|
|
94
|
+
registries?: Record<string, string | RegistryConfig>;
|
|
95
|
+
}
|
|
96
|
+
/** Where a name resolved to, and what is needed to read it. */
|
|
97
|
+
export interface ResolvedRegistryTarget {
|
|
98
|
+
url: string;
|
|
99
|
+
headers?: Record<string, string>;
|
|
100
|
+
/** The bare item name, with any namespace stripped. */
|
|
101
|
+
itemName: string;
|
|
102
|
+
}
|
|
103
|
+
/** Splits `"@acme/gauge"` into its namespace and name; a bare name gives null. */
|
|
104
|
+
export declare function splitNamespacedName(name: string): {
|
|
105
|
+
namespace: string;
|
|
106
|
+
itemName: string;
|
|
107
|
+
} | null;
|
|
108
|
+
/**
|
|
109
|
+
* Expands `${VAR}` against a lookup, so a token never has to be written into a
|
|
110
|
+
* config file that gets committed.
|
|
111
|
+
*
|
|
112
|
+
* The lookup is passed in rather than read from `process.env`, because one of
|
|
113
|
+
* the two clients has no `process`.
|
|
114
|
+
*/
|
|
115
|
+
export declare function interpolate(value: string, lookup: (name: string) => string | undefined): string;
|
|
116
|
+
/**
|
|
117
|
+
* Joins a file onto a registry base.
|
|
118
|
+
*
|
|
119
|
+
* One rule for both a URL and a filesystem path, because this module has no
|
|
120
|
+
* `path` to call and a forward slash is right for both — a local registry is
|
|
121
|
+
* how one is tested before it is deployed anywhere, so the two cannot be
|
|
122
|
+
* allowed to diverge.
|
|
123
|
+
*/
|
|
124
|
+
export declare function joinRegistryPath(base: string, file: string): string;
|
|
125
|
+
/**
|
|
126
|
+
* Resolves a possibly-namespaced component name to the URL its item is at.
|
|
127
|
+
*
|
|
128
|
+
* Pure, and exported rather than folded into a fetcher, so that URL templating
|
|
129
|
+
* and `${VAR}` expansion are testable without a network — and so the CLI and an
|
|
130
|
+
* editor cannot resolve the same name differently.
|
|
131
|
+
*/
|
|
132
|
+
export declare function resolveItemUrl(name: string, config: ComponentsConfig | null, defaultRegistryUrl: string, env?: (key: string) => string | undefined): ResolvedRegistryTarget;
|
|
133
|
+
/** Narrows a parsed JSON response, with a message naming what was asked for. */
|
|
134
|
+
export declare function assertRegistryItem(data: unknown, name: string): asserts data is RegistryItem;
|
|
135
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/registry/protocol.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry protocol: how a component's source travels from wherever it is
|
|
3
|
+
* published to whoever installs it.
|
|
4
|
+
*
|
|
5
|
+
* Modelled on shadcn/ui's, because it is the one people already know and
|
|
6
|
+
* because it is deliberately dull — two static JSON endpoints, servable by
|
|
7
|
+
* anything that serves a file:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* GET <registry>/registry.json → the index: [{ name, type, description }]
|
|
11
|
+
* GET <registry>/<name>.json → one item: its files, deps and manifest
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Kept separate from `fetch.ts`'s `fs`/`fetch` transport because this half is
|
|
15
|
+
* pure — resolving a name to a URL involves no I/O — which is what makes it
|
|
16
|
+
* unit-testable with no network, and what would let a future non-CLI client
|
|
17
|
+
* (an in-browser editor) reuse the exact same resolution rules without
|
|
18
|
+
* depending on the whole CLI.
|
|
19
|
+
*/
|
|
20
|
+
/** Splits `"@acme/gauge"` into its namespace and name; a bare name gives null. */
|
|
21
|
+
export function splitNamespacedName(name) {
|
|
22
|
+
const match = /^(@[^/]+)\/(.+)$/.exec(name);
|
|
23
|
+
return match ? { namespace: match[1], itemName: match[2] } : null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Expands `${VAR}` against a lookup, so a token never has to be written into a
|
|
27
|
+
* config file that gets committed.
|
|
28
|
+
*
|
|
29
|
+
* The lookup is passed in rather than read from `process.env`, because one of
|
|
30
|
+
* the two clients has no `process`.
|
|
31
|
+
*/
|
|
32
|
+
export function interpolate(value, lookup) {
|
|
33
|
+
return value.replace(/\$\{([A-Za-z0-9_]+)\}/g, (whole, name) => lookup(name) ?? whole);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Joins a file onto a registry base.
|
|
37
|
+
*
|
|
38
|
+
* One rule for both a URL and a filesystem path, because this module has no
|
|
39
|
+
* `path` to call and a forward slash is right for both — a local registry is
|
|
40
|
+
* how one is tested before it is deployed anywhere, so the two cannot be
|
|
41
|
+
* allowed to diverge.
|
|
42
|
+
*/
|
|
43
|
+
export function joinRegistryPath(base, file) {
|
|
44
|
+
return `${base.replace(/\/$/, "")}/${file}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolves a possibly-namespaced component name to the URL its item is at.
|
|
48
|
+
*
|
|
49
|
+
* Pure, and exported rather than folded into a fetcher, so that URL templating
|
|
50
|
+
* and `${VAR}` expansion are testable without a network — and so the CLI and an
|
|
51
|
+
* editor cannot resolve the same name differently.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveItemUrl(name, config, defaultRegistryUrl, env = () => undefined) {
|
|
54
|
+
const namespaced = splitNamespacedName(name);
|
|
55
|
+
if (!namespaced) {
|
|
56
|
+
return {
|
|
57
|
+
url: joinRegistryPath(defaultRegistryUrl, `${name}.json`),
|
|
58
|
+
itemName: name,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const entry = config?.registries?.[namespaced.namespace];
|
|
62
|
+
if (!entry) {
|
|
63
|
+
throw new Error(`Unknown registry namespace "${namespaced.namespace}" — add it under "registries" in motionscript.json.`);
|
|
64
|
+
}
|
|
65
|
+
const registry = typeof entry === "string" ? { url: entry } : entry;
|
|
66
|
+
if (!registry.url.includes("{name}")) {
|
|
67
|
+
throw new Error(`Registry "${namespaced.namespace}" URL must contain a literal "{name}" placeholder.`);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
url: registry.url.replace("{name}", namespaced.itemName),
|
|
71
|
+
headers: registry.headers
|
|
72
|
+
? Object.fromEntries(Object.entries(registry.headers).map(([key, value]) => [
|
|
73
|
+
key,
|
|
74
|
+
interpolate(value, env),
|
|
75
|
+
]))
|
|
76
|
+
: undefined,
|
|
77
|
+
itemName: namespaced.itemName,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Narrows a parsed JSON response, with a message naming what was asked for. */
|
|
81
|
+
export function assertRegistryItem(data, name) {
|
|
82
|
+
if (!data ||
|
|
83
|
+
typeof data !== "object" ||
|
|
84
|
+
!Array.isArray(data.files)) {
|
|
85
|
+
throw new Error(`Malformed registry item for "${name}" (expected an object with a "files" array).`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/registry/protocol.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;GAkBG;AAwFH,kFAAkF;AAClF,MAAM,UAAU,mBAAmB,CACjC,IAAY;IAEZ,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC3C,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACnE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,KAAa,EACb,MAA4C;IAE5C,OAAO,KAAK,CAAC,OAAO,CAClB,wBAAwB,EACxB,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,CAC/C,CAAA;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,IAAY;IACzD,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,IAAI,EAAE,CAAA;AAC7C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,MAA+B,EAC/B,kBAA0B,EAC1B,MAA2C,GAAG,EAAE,CAAC,SAAS;IAE1D,MAAM,UAAU,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAA;IAC5C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO;YACL,GAAG,EAAE,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,IAAI,OAAO,CAAC;YACzD,QAAQ,EAAE,IAAI;SACf,CAAA;IACH,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,EAAE,UAAU,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;IACxD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,+BAA+B,UAAU,CAAC,SAAS,qDAAqD,CACzG,CAAA;IACH,CAAC;IACD,MAAM,QAAQ,GACZ,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;IACpD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,aAAa,UAAU,CAAC,SAAS,oDAAoD,CACtF,CAAA;IACH,CAAC;IAED,OAAO;QACL,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,UAAU,CAAC,QAAQ,CAAC;QACxD,OAAO,EAAE,QAAQ,CAAC,OAAO;YACvB,CAAC,CAAC,MAAM,CAAC,WAAW,CAChB,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC;gBACrD,GAAG;gBACH,WAAW,CAAC,KAAK,EAAE,GAAG,CAAC;aACxB,CAAC,CACH;YACH,CAAC,CAAC,SAAS;QACb,QAAQ,EAAE,UAAU,CAAC,QAAQ;KAC9B,CAAA;AACH,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,kBAAkB,CAChC,IAAa,EACb,IAAY;IAEZ,IACE,CAAC,IAAI;QACL,OAAO,IAAI,KAAK,QAAQ;QACxB,CAAC,KAAK,CAAC,OAAO,CAAE,IAA4B,CAAC,KAAK,CAAC,EACnD,CAAC;QACD,MAAM,IAAI,KAAK,CACb,gCAAgC,IAAI,8CAA8C,CACnF,CAAA;IACH,CAAC;AACH,CAAC","sourcesContent":["\n/**\n * The registry protocol: how a component's source travels from wherever it is\n * published to whoever installs it.\n *\n * Modelled on shadcn/ui's, because it is the one people already know and\n * because it is deliberately dull — two static JSON endpoints, servable by\n * anything that serves a file:\n *\n * ```\n * GET <registry>/registry.json → the index: [{ name, type, description }]\n * GET <registry>/<name>.json → one item: its files, deps and manifest\n * ```\n *\n * Kept separate from `fetch.ts`'s `fs`/`fetch` transport because this half is\n * pure — resolving a name to a URL involves no I/O — which is what makes it\n * unit-testable with no network, and what would let a future non-CLI client\n * (an in-browser editor) reuse the exact same resolution rules without\n * depending on the whole CLI.\n */\n\nexport type RegistryItemType = \"registry:component\" | \"registry:kit\"\n\n/** One row of a registry's index. */\nexport interface RegistryIndexEntry {\n name: string\n type: RegistryItemType\n description?: string\n /** The version this item was cut from. */\n version?: string\n /** Whether its source may be copied at all. */\n forkable?: boolean\n}\n\n/** One file of an item, with its content inlined. */\nexport interface RegistryFile {\n /** Path relative to the components root, e.g. `\"line-chart/chart-line.ts\"`. */\n path: string\n content: string\n}\n\n/** A single installable component. */\nexport interface RegistryItem {\n name: string\n type: RegistryItemType\n description?: string\n /** The version this was cut from, so a lock can record what it installed. */\n version?: string\n /** npm dependencies, as `\"pkg@range\"`. */\n dependencies?: string[]\n /** Other item names, installed first. */\n registryDependencies?: string[]\n files: RegistryFile[]\n /**\n * The kit these files import and must **not** copy, as `\"pkg@range\"`.\n *\n * Always also present in {@link dependencies}; named separately because a\n * host that copies source has to know which bare specifier must resolve to\n * its own single copy rather than to a second one it installed alongside.\n */\n kit?: string\n forkable?: boolean\n /** sha256 over the canonicalised file list, so a copy can be verified. */\n integrity?: string\n /**\n * Whether a host with no package manager can install this: true only when\n * every entry of {@link dependencies} is something it already bundles.\n *\n * An editor installs *source*, never npm. Declaring this lets it say \"needs\n * `@acme/foo`, which cannot be installed here\" while the item is still a card\n * in a list, rather than failing when the code is linked.\n */\n standalone?: boolean\n}\n\n/** A configured registry: a URL template, plus whatever it needs to be read. */\nexport interface RegistryConfig {\n /** Contains a literal `{name}` placeholder. */\n url: string\n headers?: Record<string, string>\n params?: Record<string, string>\n}\n\n/** The `motionscript.json` a project keeps at its root. */\nexport interface ComponentsConfig {\n $schema?: string\n project: string\n aliases: { components: string }\n paths: { components: string }\n /**\n * Additional registries, keyed by `\"@namespace\"`.\n *\n * The default registry is never configured here — it is the client's own\n * constant — matching shadcn's `components.json`, whose `registries` keys are\n * schema-constrained to `^@`.\n */\n registries?: Record<string, string | RegistryConfig>\n}\n\n/** Where a name resolved to, and what is needed to read it. */\nexport interface ResolvedRegistryTarget {\n url: string\n headers?: Record<string, string>\n /** The bare item name, with any namespace stripped. */\n itemName: string\n}\n\n/** Splits `\"@acme/gauge\"` into its namespace and name; a bare name gives null. */\nexport function splitNamespacedName(\n name: string\n): { namespace: string; itemName: string } | null {\n const match = /^(@[^/]+)\\/(.+)$/.exec(name)\n return match ? { namespace: match[1], itemName: match[2] } : null\n}\n\n/**\n * Expands `${VAR}` against a lookup, so a token never has to be written into a\n * config file that gets committed.\n *\n * The lookup is passed in rather than read from `process.env`, because one of\n * the two clients has no `process`.\n */\nexport function interpolate(\n value: string,\n lookup: (name: string) => string | undefined\n): string {\n return value.replace(\n /\\$\\{([A-Za-z0-9_]+)\\}/g,\n (whole, name: string) => lookup(name) ?? whole\n )\n}\n\n/**\n * Joins a file onto a registry base.\n *\n * One rule for both a URL and a filesystem path, because this module has no\n * `path` to call and a forward slash is right for both — a local registry is\n * how one is tested before it is deployed anywhere, so the two cannot be\n * allowed to diverge.\n */\nexport function joinRegistryPath(base: string, file: string): string {\n return `${base.replace(/\\/$/, \"\")}/${file}`\n}\n\n/**\n * Resolves a possibly-namespaced component name to the URL its item is at.\n *\n * Pure, and exported rather than folded into a fetcher, so that URL templating\n * and `${VAR}` expansion are testable without a network — and so the CLI and an\n * editor cannot resolve the same name differently.\n */\nexport function resolveItemUrl(\n name: string,\n config: ComponentsConfig | null,\n defaultRegistryUrl: string,\n env: (key: string) => string | undefined = () => undefined\n): ResolvedRegistryTarget {\n const namespaced = splitNamespacedName(name)\n if (!namespaced) {\n return {\n url: joinRegistryPath(defaultRegistryUrl, `${name}.json`),\n itemName: name,\n }\n }\n\n const entry = config?.registries?.[namespaced.namespace]\n if (!entry) {\n throw new Error(\n `Unknown registry namespace \"${namespaced.namespace}\" — add it under \"registries\" in motionscript.json.`\n )\n }\n const registry: RegistryConfig =\n typeof entry === \"string\" ? { url: entry } : entry\n if (!registry.url.includes(\"{name}\")) {\n throw new Error(\n `Registry \"${namespaced.namespace}\" URL must contain a literal \"{name}\" placeholder.`\n )\n }\n\n return {\n url: registry.url.replace(\"{name}\", namespaced.itemName),\n headers: registry.headers\n ? Object.fromEntries(\n Object.entries(registry.headers).map(([key, value]) => [\n key,\n interpolate(value, env),\n ])\n )\n : undefined,\n itemName: namespaced.itemName,\n }\n}\n\n/** Narrows a parsed JSON response, with a message naming what was asked for. */\nexport function assertRegistryItem(\n data: unknown,\n name: string\n): asserts data is RegistryItem {\n if (\n !data ||\n typeof data !== \"object\" ||\n !Array.isArray((data as { files?: unknown }).files)\n ) {\n throw new Error(\n `Malformed registry item for \"${name}\" (expected an object with a \"files\" array).`\n )\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motionscript/cli",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Copy Motion Script component source into your project: ms init, ms add, ms list, ms diff.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/motionscript-dev/motionscript.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://motionscript.dev",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/motionscript-dev/motionscript/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"ms": "dist/cli.js",
|
|
17
|
+
"motion-script": "dist/cli.js",
|
|
18
|
+
"motionscript": "dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"main": "dist/cli.js",
|
|
21
|
+
"types": "dist/cli.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"!dist/**/*.tsbuildinfo"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"motion-script",
|
|
35
|
+
"motion script",
|
|
36
|
+
"cli",
|
|
37
|
+
"components",
|
|
38
|
+
"registry"
|
|
39
|
+
],
|
|
40
|
+
"license": "Apache-2.0",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"jsonc-parser": "^3.3.1",
|
|
43
|
+
"kleur": "^4.1.5",
|
|
44
|
+
"minimist": "^1.2.8",
|
|
45
|
+
"prompts": "^2.4.2"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/minimist": "^1.2.5",
|
|
49
|
+
"@types/node": "^25.6.2",
|
|
50
|
+
"@types/prompts": "^2.4.9",
|
|
51
|
+
"eslint": "^9.39.4",
|
|
52
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
53
|
+
"eslint-plugin-import-x": "^4.16.1",
|
|
54
|
+
"tsc-alias": "^1.8.16",
|
|
55
|
+
"typescript": "^6.0.3",
|
|
56
|
+
"typescript-eslint": "^8.48.0",
|
|
57
|
+
"vitest": "^4.1.5"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
|
|
61
|
+
"dev": "tsc -p tsconfig.build.json --watch & tsc-alias -p tsconfig.build.json -w",
|
|
62
|
+
"clean": "rimraf --glob dist .turbo *.tsbuildinfo",
|
|
63
|
+
"test": "vitest --passWithNoTests",
|
|
64
|
+
"lint": "eslint .",
|
|
65
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
66
|
+
}
|
|
6
67
|
}
|
package/README.md
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
# Temporary Holding Version
|
|
2
|
-
|
|
3
|
-
This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
|
|
4
|
-
If no other versions are published within 30 days, this package and version will be deleted.
|