@c9up/nebula 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/configure.d.ts +2 -0
- package/dist/configure.js +6 -0
- package/dist/console/AddCommand.d.ts +28 -0
- package/dist/console/AddCommand.js +84 -0
- package/dist/console/ListCommand.d.ts +17 -0
- package/dist/console/ListCommand.js +49 -0
- package/dist/console/contract.d.ts +63 -0
- package/dist/console/contract.js +39 -0
- package/package.json +9 -1
- package/src/configure.ts +10 -0
- package/src/console/AddCommand.ts +104 -0
- package/src/console/ListCommand.ts +61 -0
- package/src/console/contract.ts +98 -0
package/README.md
CHANGED
|
@@ -40,9 +40,15 @@ ream nebula:add dialog data-table # copies both, plus what they depend on
|
|
|
40
40
|
ream nebula:add button --force # overwrite your edited copy
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
`
|
|
45
|
-
|
|
43
|
+
Command classes shipped by this package and registered in `reamrc.commands` by
|
|
44
|
+
`configure()` — the channel Ream provides for exactly this, which directory
|
|
45
|
+
discovery cannot see. `ream` forwards any unrecognised name to the app's
|
|
46
|
+
console kernel, so nothing is compiled into the binary and a new nebula command
|
|
47
|
+
never waits on a release of it.
|
|
48
|
+
|
|
49
|
+
Like atlas's seven migration commands, and for the same reason, this needs no
|
|
50
|
+
dependency on `@c9up/ream`: `src/console/contract.ts` declares the shape the
|
|
51
|
+
kernel dispatches against rather than importing it.
|
|
46
52
|
|
|
47
53
|
Copies mirror the package's own layout, so `atoms/Button` finds `../lib/cva.js` for the same reason it does inside nebula. **No import is ever rewritten** — that is where a copy-the-source CLI usually accumulates its edge cases.
|
|
48
54
|
|
package/dist/configure.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ interface Codemods {
|
|
|
27
27
|
writeFile(filePath: string, content: string, options?: {
|
|
28
28
|
force?: boolean;
|
|
29
29
|
}): Promise<void>;
|
|
30
|
+
/** Appends to `reamrc.commands` — the channel for package-shipped commands. */
|
|
31
|
+
registerCommand(importPath: string): Promise<void>;
|
|
30
32
|
}
|
|
31
33
|
/** Flags forwarded from `ream add` / `ream configure`, as the CLI encodes them. */
|
|
32
34
|
type Flags = Record<string, string[] | undefined>;
|
package/dist/configure.js
CHANGED
|
@@ -59,6 +59,12 @@ export async function configure(codemods, flags = {}) {
|
|
|
59
59
|
const adapter = adapterFor(name);
|
|
60
60
|
const config = resolveConfig({ adapter: name });
|
|
61
61
|
await codemods.writeFile("config/nebula.ts", configFile(name));
|
|
62
|
+
// `reamrc.commands` is the channel Ream provides for commands a package
|
|
63
|
+
// ships, which directory discovery cannot see. `ream` forwards any
|
|
64
|
+
// unrecognised name to the app's console kernel, so these exist without a
|
|
65
|
+
// line of Rust and without waiting on a release of the binary.
|
|
66
|
+
await codemods.registerCommand("@c9up/nebula/commands/add");
|
|
67
|
+
await codemods.registerCommand("@c9up/nebula/commands/list");
|
|
62
68
|
for (const file of adapter.files(config)) {
|
|
63
69
|
await codemods.writeFile(file.path, file.contents);
|
|
64
70
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream nebula:add <component…>` — copy components into the project.
|
|
3
|
+
*
|
|
4
|
+
* A command class registered through `reamrc.commands`, which is the channel
|
|
5
|
+
* Ream provides for commands a package ships and directory discovery cannot
|
|
6
|
+
* see. `ream` dispatches any unrecognised name to the app's console kernel,
|
|
7
|
+
* so nothing has to be added to the binary for this to exist.
|
|
8
|
+
*
|
|
9
|
+
* That matters more than it looks: the alternative — a subcommand compiled
|
|
10
|
+
* into `ream-cli` — makes every package that wants a command wait on a release
|
|
11
|
+
* of a Rust binary, and that binary is not on crates.io.
|
|
12
|
+
*/
|
|
13
|
+
export default class NebulaAddCommand {
|
|
14
|
+
static commandName: string;
|
|
15
|
+
static description: string;
|
|
16
|
+
static options: {
|
|
17
|
+
startApp: boolean;
|
|
18
|
+
};
|
|
19
|
+
static args: import("./contract.js").ArgumentMetaData[];
|
|
20
|
+
static flags: import("./contract.js").FlagMetaData[];
|
|
21
|
+
static help: string[];
|
|
22
|
+
components: string[];
|
|
23
|
+
force: boolean;
|
|
24
|
+
dryRun: boolean;
|
|
25
|
+
ts: boolean;
|
|
26
|
+
js: boolean;
|
|
27
|
+
run(): Promise<void>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream nebula:add <component…>` — copy components into the project.
|
|
3
|
+
*
|
|
4
|
+
* A command class registered through `reamrc.commands`, which is the channel
|
|
5
|
+
* Ream provides for commands a package ships and directory discovery cannot
|
|
6
|
+
* see. `ream` dispatches any unrecognised name to the app's console kernel,
|
|
7
|
+
* so nothing has to be added to the binary for this to exist.
|
|
8
|
+
*
|
|
9
|
+
* That matters more than it looks: the alternative — a subcommand compiled
|
|
10
|
+
* into `ream-cli` — makes every package that wants a command wait on a release
|
|
11
|
+
* of a Rust binary, and that binary is not on crates.io.
|
|
12
|
+
*/
|
|
13
|
+
import { add } from "../cli/add.js";
|
|
14
|
+
import { argument, flag } from "./contract.js";
|
|
15
|
+
export default class NebulaAddCommand {
|
|
16
|
+
static commandName = "nebula:add";
|
|
17
|
+
static description = "Copy nebula components into the project — they become yours to edit";
|
|
18
|
+
// Files only; nothing here needs the container or a booted app.
|
|
19
|
+
static options = { startApp: false };
|
|
20
|
+
static args = [
|
|
21
|
+
argument("components", {
|
|
22
|
+
type: "spread",
|
|
23
|
+
description: "Component names, as `ream nebula:list` prints them",
|
|
24
|
+
}),
|
|
25
|
+
];
|
|
26
|
+
static flags = [
|
|
27
|
+
flag("force", "boolean", {
|
|
28
|
+
description: "Overwrite files that already exist",
|
|
29
|
+
}),
|
|
30
|
+
flag("dryRun", "boolean", {
|
|
31
|
+
description: "Print what would happen and write nothing",
|
|
32
|
+
}),
|
|
33
|
+
flag("ts", "boolean", {
|
|
34
|
+
description: "Copy TypeScript sources instead of the compiled output",
|
|
35
|
+
}),
|
|
36
|
+
flag("js", "boolean", {
|
|
37
|
+
description: "Copy the compiled JavaScript (the default for an unbuilt app)",
|
|
38
|
+
}),
|
|
39
|
+
];
|
|
40
|
+
static help = [
|
|
41
|
+
" ream nebula:add button card",
|
|
42
|
+
" ream nebula:add dialog --force",
|
|
43
|
+
"",
|
|
44
|
+
"Components are copied, not linked. Once they are in your project they are",
|
|
45
|
+
"yours: edit the class strings, delete what you do not use, and nothing",
|
|
46
|
+
"upgrades them behind your back.",
|
|
47
|
+
];
|
|
48
|
+
components = [];
|
|
49
|
+
force = false;
|
|
50
|
+
dryRun = false;
|
|
51
|
+
ts = false;
|
|
52
|
+
js = false;
|
|
53
|
+
async run() {
|
|
54
|
+
if (this.components.length === 0) {
|
|
55
|
+
process.stderr.write("Name at least one component — `ream nebula:add button`.\n" +
|
|
56
|
+
"`ream nebula:list` shows what the registry holds.\n");
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const result = add({
|
|
61
|
+
cwd: process.cwd(),
|
|
62
|
+
names: this.components,
|
|
63
|
+
force: this.force,
|
|
64
|
+
dryRun: this.dryRun,
|
|
65
|
+
language: this.ts ? "ts" : this.js ? "js" : undefined,
|
|
66
|
+
});
|
|
67
|
+
const out = [""];
|
|
68
|
+
for (const path of result.written)
|
|
69
|
+
out.push(` create ${path}`);
|
|
70
|
+
for (const path of result.skipped)
|
|
71
|
+
out.push(` exists ${path}`);
|
|
72
|
+
if (result.skipped.length > 0) {
|
|
73
|
+
out.push("", " Existing files were left alone. Re-run with --force to overwrite.");
|
|
74
|
+
}
|
|
75
|
+
// Stated rather than assumed: the language is inferred from the tree in
|
|
76
|
+
// most runs, and copying the wrong one writes files the project cannot
|
|
77
|
+
// load — a silent failure worth one line to prevent.
|
|
78
|
+
out.push("", ` Copied as ${result.language === "ts" ? "TypeScript" : "JavaScript"}. Override with --ts or --js.`, "");
|
|
79
|
+
process.stdout.write(out.join("\n"));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Fails the build if the class drifts from what the kernel dispatches against.
|
|
83
|
+
const _contract = NebulaAddCommand;
|
|
84
|
+
void _contract;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream nebula:list` — show what the registry holds.
|
|
3
|
+
*
|
|
4
|
+
* Reads `registry.json` out of the installed package, which is why this lives
|
|
5
|
+
* here and not in the binary: the CLI can neither resolve where nebula is
|
|
6
|
+
* installed nor parse a format that belongs to this repo.
|
|
7
|
+
*/
|
|
8
|
+
export default class NebulaListCommand {
|
|
9
|
+
static commandName: string;
|
|
10
|
+
static description: string;
|
|
11
|
+
static options: {
|
|
12
|
+
startApp: boolean;
|
|
13
|
+
};
|
|
14
|
+
static flags: import("./contract.js").FlagMetaData[];
|
|
15
|
+
layer?: string;
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream nebula:list` — show what the registry holds.
|
|
3
|
+
*
|
|
4
|
+
* Reads `registry.json` out of the installed package, which is why this lives
|
|
5
|
+
* here and not in the binary: the CLI can neither resolve where nebula is
|
|
6
|
+
* installed nor parse a format that belongs to this repo.
|
|
7
|
+
*/
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { loadRegistry, packageRoot } from "../cli/registry.js";
|
|
10
|
+
import { flag } from "./contract.js";
|
|
11
|
+
const LAYERS = ["atoms", "molecules", "organisms", "templates"];
|
|
12
|
+
export default class NebulaListCommand {
|
|
13
|
+
static commandName = "nebula:list";
|
|
14
|
+
static description = "List the nebula components available to `ream nebula:add`";
|
|
15
|
+
static options = { startApp: false };
|
|
16
|
+
static flags = [
|
|
17
|
+
flag("layer", "string", {
|
|
18
|
+
description: `Restrict to one atomic layer (${LAYERS.join(", ")})`,
|
|
19
|
+
}),
|
|
20
|
+
];
|
|
21
|
+
layer;
|
|
22
|
+
async run() {
|
|
23
|
+
const root = packageRoot(join(process.cwd(), "package.json"));
|
|
24
|
+
const registry = loadRegistry(join(root, "registry.json"));
|
|
25
|
+
if (this.layer !== undefined &&
|
|
26
|
+
!LAYERS.some((name) => name === this.layer)) {
|
|
27
|
+
// Named but unknown: a typo would otherwise print nothing at all and
|
|
28
|
+
// read as "the registry is empty".
|
|
29
|
+
process.stderr.write(`Unknown layer "${this.layer}" — one of ${LAYERS.join(", ")}.\n`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const layer of LAYERS) {
|
|
35
|
+
if (this.layer !== undefined && this.layer !== layer)
|
|
36
|
+
continue;
|
|
37
|
+
const names = registry.items
|
|
38
|
+
.filter((item) => item.layer === layer)
|
|
39
|
+
.map((item) => item.name);
|
|
40
|
+
if (names.length === 0)
|
|
41
|
+
continue;
|
|
42
|
+
out.push("", ` ${layer} (${names.length})`, ` ${names.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
out.push("");
|
|
45
|
+
process.stdout.write(out.join("\n"));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const _contract = NebulaListCommand;
|
|
49
|
+
void _contract;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console-command contract, declared locally.
|
|
3
|
+
*
|
|
4
|
+
* nebula must not import `@c9up/ream`. It is a component package whose only
|
|
5
|
+
* peer is aurora, and taking on the framework to describe two commands would
|
|
6
|
+
* be the tail wagging the dog. Atlas reached the same conclusion for its seven
|
|
7
|
+
* migration commands and declares the same shape in its own `contract.ts`;
|
|
8
|
+
* this is that decision, repeated because the reason is the same.
|
|
9
|
+
*
|
|
10
|
+
* What the kernel actually needs is structural: a class with `commandName`,
|
|
11
|
+
* `description` and a `run()`, plus the metadata it reads to parse argv into
|
|
12
|
+
* instance properties. Ream's `@args` / `@flags` decorators build that
|
|
13
|
+
* metadata; these helpers build the identical shape without them.
|
|
14
|
+
*/
|
|
15
|
+
export interface CommandOptions {
|
|
16
|
+
/** Boot the application before `run()`. Off — nebula touches only files. */
|
|
17
|
+
startApp?: boolean;
|
|
18
|
+
staysAlive?: boolean;
|
|
19
|
+
allowUnknownFlags?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ArgumentMetaData {
|
|
22
|
+
type: "string" | "spread";
|
|
23
|
+
propertyName: string;
|
|
24
|
+
argumentName: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
required: boolean;
|
|
27
|
+
default?: string | string[];
|
|
28
|
+
}
|
|
29
|
+
export interface FlagMetaData {
|
|
30
|
+
type: "string" | "boolean" | "number" | "array";
|
|
31
|
+
propertyName: string;
|
|
32
|
+
flagName: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
alias: string[];
|
|
35
|
+
default?: string | string[] | number | boolean;
|
|
36
|
+
required: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** The static side the kernel reads. */
|
|
39
|
+
export interface NebulaCommandClass {
|
|
40
|
+
new (): {
|
|
41
|
+
run(): Promise<void> | void;
|
|
42
|
+
};
|
|
43
|
+
commandName: string;
|
|
44
|
+
description: string;
|
|
45
|
+
options?: CommandOptions;
|
|
46
|
+
args?: readonly ArgumentMetaData[];
|
|
47
|
+
flags?: readonly FlagMetaData[];
|
|
48
|
+
help?: string | string[];
|
|
49
|
+
}
|
|
50
|
+
export declare function flag(propertyName: string, type: FlagMetaData["type"], options?: {
|
|
51
|
+
flagName?: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
alias?: string[];
|
|
54
|
+
default?: FlagMetaData["default"];
|
|
55
|
+
required?: boolean;
|
|
56
|
+
}): FlagMetaData;
|
|
57
|
+
export declare function argument(propertyName: string, options?: {
|
|
58
|
+
type?: ArgumentMetaData["type"];
|
|
59
|
+
argumentName?: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
required?: boolean;
|
|
62
|
+
default?: ArgumentMetaData["default"];
|
|
63
|
+
}): ArgumentMetaData;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console-command contract, declared locally.
|
|
3
|
+
*
|
|
4
|
+
* nebula must not import `@c9up/ream`. It is a component package whose only
|
|
5
|
+
* peer is aurora, and taking on the framework to describe two commands would
|
|
6
|
+
* be the tail wagging the dog. Atlas reached the same conclusion for its seven
|
|
7
|
+
* migration commands and declares the same shape in its own `contract.ts`;
|
|
8
|
+
* this is that decision, repeated because the reason is the same.
|
|
9
|
+
*
|
|
10
|
+
* What the kernel actually needs is structural: a class with `commandName`,
|
|
11
|
+
* `description` and a `run()`, plus the metadata it reads to parse argv into
|
|
12
|
+
* instance properties. Ream's `@args` / `@flags` decorators build that
|
|
13
|
+
* metadata; these helpers build the identical shape without them.
|
|
14
|
+
*/
|
|
15
|
+
/** `dryRun` → `dry-run`, matching what the framework's decorators produce. */
|
|
16
|
+
function dashCase(value) {
|
|
17
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
export function flag(propertyName, type, options = {}) {
|
|
20
|
+
return {
|
|
21
|
+
type,
|
|
22
|
+
propertyName,
|
|
23
|
+
flagName: options.flagName ?? dashCase(propertyName),
|
|
24
|
+
description: options.description,
|
|
25
|
+
alias: options.alias ?? [],
|
|
26
|
+
default: options.default,
|
|
27
|
+
required: options.required ?? false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function argument(propertyName, options = {}) {
|
|
31
|
+
return {
|
|
32
|
+
type: options.type ?? "string",
|
|
33
|
+
propertyName,
|
|
34
|
+
argumentName: options.argumentName ?? dashCase(propertyName),
|
|
35
|
+
description: options.description,
|
|
36
|
+
required: options.required ?? options.default === undefined,
|
|
37
|
+
default: options.default,
|
|
38
|
+
};
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/nebula",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Nebula — shadcn/ui ported to Aurora, organised as atomic design. Zero-dependency headless primitives, cva reimplemented, copy-the-source registry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -46,6 +46,14 @@
|
|
|
46
46
|
"./cli": {
|
|
47
47
|
"types": "./dist/cli/index.d.ts",
|
|
48
48
|
"import": "./dist/cli/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./commands/add": {
|
|
51
|
+
"types": "./dist/console/AddCommand.d.ts",
|
|
52
|
+
"import": "./dist/console/AddCommand.js"
|
|
53
|
+
},
|
|
54
|
+
"./commands/list": {
|
|
55
|
+
"types": "./dist/console/ListCommand.d.ts",
|
|
56
|
+
"import": "./dist/console/ListCommand.js"
|
|
49
57
|
}
|
|
50
58
|
},
|
|
51
59
|
"peerDependencies": {
|
package/src/configure.ts
CHANGED
|
@@ -33,6 +33,8 @@ interface Codemods {
|
|
|
33
33
|
content: string,
|
|
34
34
|
options?: { force?: boolean },
|
|
35
35
|
): Promise<void>;
|
|
36
|
+
/** Appends to `reamrc.commands` — the channel for package-shipped commands. */
|
|
37
|
+
registerCommand(importPath: string): Promise<void>;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/** Flags forwarded from `ream add` / `ream configure`, as the CLI encodes them. */
|
|
@@ -87,6 +89,14 @@ export async function configure(
|
|
|
87
89
|
const config = resolveConfig({ adapter: name });
|
|
88
90
|
|
|
89
91
|
await codemods.writeFile("config/nebula.ts", configFile(name));
|
|
92
|
+
|
|
93
|
+
// `reamrc.commands` is the channel Ream provides for commands a package
|
|
94
|
+
// ships, which directory discovery cannot see. `ream` forwards any
|
|
95
|
+
// unrecognised name to the app's console kernel, so these exist without a
|
|
96
|
+
// line of Rust and without waiting on a release of the binary.
|
|
97
|
+
await codemods.registerCommand("@c9up/nebula/commands/add");
|
|
98
|
+
await codemods.registerCommand("@c9up/nebula/commands/list");
|
|
99
|
+
|
|
90
100
|
for (const file of adapter.files(config)) {
|
|
91
101
|
await codemods.writeFile(file.path, file.contents);
|
|
92
102
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream nebula:add <component…>` — copy components into the project.
|
|
3
|
+
*
|
|
4
|
+
* A command class registered through `reamrc.commands`, which is the channel
|
|
5
|
+
* Ream provides for commands a package ships and directory discovery cannot
|
|
6
|
+
* see. `ream` dispatches any unrecognised name to the app's console kernel,
|
|
7
|
+
* so nothing has to be added to the binary for this to exist.
|
|
8
|
+
*
|
|
9
|
+
* That matters more than it looks: the alternative — a subcommand compiled
|
|
10
|
+
* into `ream-cli` — makes every package that wants a command wait on a release
|
|
11
|
+
* of a Rust binary, and that binary is not on crates.io.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { add } from "../cli/add.js";
|
|
15
|
+
import { argument, flag, type NebulaCommandClass } from "./contract.js";
|
|
16
|
+
|
|
17
|
+
export default class NebulaAddCommand {
|
|
18
|
+
static commandName = "nebula:add";
|
|
19
|
+
static description =
|
|
20
|
+
"Copy nebula components into the project — they become yours to edit";
|
|
21
|
+
|
|
22
|
+
// Files only; nothing here needs the container or a booted app.
|
|
23
|
+
static options = { startApp: false };
|
|
24
|
+
|
|
25
|
+
static args = [
|
|
26
|
+
argument("components", {
|
|
27
|
+
type: "spread",
|
|
28
|
+
description: "Component names, as `ream nebula:list` prints them",
|
|
29
|
+
}),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
static flags = [
|
|
33
|
+
flag("force", "boolean", {
|
|
34
|
+
description: "Overwrite files that already exist",
|
|
35
|
+
}),
|
|
36
|
+
flag("dryRun", "boolean", {
|
|
37
|
+
description: "Print what would happen and write nothing",
|
|
38
|
+
}),
|
|
39
|
+
flag("ts", "boolean", {
|
|
40
|
+
description: "Copy TypeScript sources instead of the compiled output",
|
|
41
|
+
}),
|
|
42
|
+
flag("js", "boolean", {
|
|
43
|
+
description:
|
|
44
|
+
"Copy the compiled JavaScript (the default for an unbuilt app)",
|
|
45
|
+
}),
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
static help = [
|
|
49
|
+
" ream nebula:add button card",
|
|
50
|
+
" ream nebula:add dialog --force",
|
|
51
|
+
"",
|
|
52
|
+
"Components are copied, not linked. Once they are in your project they are",
|
|
53
|
+
"yours: edit the class strings, delete what you do not use, and nothing",
|
|
54
|
+
"upgrades them behind your back.",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
components: string[] = [];
|
|
58
|
+
force = false;
|
|
59
|
+
dryRun = false;
|
|
60
|
+
ts = false;
|
|
61
|
+
js = false;
|
|
62
|
+
|
|
63
|
+
async run(): Promise<void> {
|
|
64
|
+
if (this.components.length === 0) {
|
|
65
|
+
process.stderr.write(
|
|
66
|
+
"Name at least one component — `ream nebula:add button`.\n" +
|
|
67
|
+
"`ream nebula:list` shows what the registry holds.\n",
|
|
68
|
+
);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = add({
|
|
74
|
+
cwd: process.cwd(),
|
|
75
|
+
names: this.components,
|
|
76
|
+
force: this.force,
|
|
77
|
+
dryRun: this.dryRun,
|
|
78
|
+
language: this.ts ? "ts" : this.js ? "js" : undefined,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const out: string[] = [""];
|
|
82
|
+
for (const path of result.written) out.push(` create ${path}`);
|
|
83
|
+
for (const path of result.skipped) out.push(` exists ${path}`);
|
|
84
|
+
if (result.skipped.length > 0) {
|
|
85
|
+
out.push(
|
|
86
|
+
"",
|
|
87
|
+
" Existing files were left alone. Re-run with --force to overwrite.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
// Stated rather than assumed: the language is inferred from the tree in
|
|
91
|
+
// most runs, and copying the wrong one writes files the project cannot
|
|
92
|
+
// load — a silent failure worth one line to prevent.
|
|
93
|
+
out.push(
|
|
94
|
+
"",
|
|
95
|
+
` Copied as ${result.language === "ts" ? "TypeScript" : "JavaScript"}. Override with --ts or --js.`,
|
|
96
|
+
"",
|
|
97
|
+
);
|
|
98
|
+
process.stdout.write(out.join("\n"));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Fails the build if the class drifts from what the kernel dispatches against.
|
|
103
|
+
const _contract: NebulaCommandClass = NebulaAddCommand;
|
|
104
|
+
void _contract;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ream nebula:list` — show what the registry holds.
|
|
3
|
+
*
|
|
4
|
+
* Reads `registry.json` out of the installed package, which is why this lives
|
|
5
|
+
* here and not in the binary: the CLI can neither resolve where nebula is
|
|
6
|
+
* installed nor parse a format that belongs to this repo.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { loadRegistry, packageRoot } from "../cli/registry.js";
|
|
11
|
+
import { flag, type NebulaCommandClass } from "./contract.js";
|
|
12
|
+
|
|
13
|
+
const LAYERS = ["atoms", "molecules", "organisms", "templates"] as const;
|
|
14
|
+
|
|
15
|
+
export default class NebulaListCommand {
|
|
16
|
+
static commandName = "nebula:list";
|
|
17
|
+
static description =
|
|
18
|
+
"List the nebula components available to `ream nebula:add`";
|
|
19
|
+
static options = { startApp: false };
|
|
20
|
+
|
|
21
|
+
static flags = [
|
|
22
|
+
flag("layer", "string", {
|
|
23
|
+
description: `Restrict to one atomic layer (${LAYERS.join(", ")})`,
|
|
24
|
+
}),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
layer?: string;
|
|
28
|
+
|
|
29
|
+
async run(): Promise<void> {
|
|
30
|
+
const root = packageRoot(join(process.cwd(), "package.json"));
|
|
31
|
+
const registry = loadRegistry(join(root, "registry.json"));
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
this.layer !== undefined &&
|
|
35
|
+
!LAYERS.some((name) => name === this.layer)
|
|
36
|
+
) {
|
|
37
|
+
// Named but unknown: a typo would otherwise print nothing at all and
|
|
38
|
+
// read as "the registry is empty".
|
|
39
|
+
process.stderr.write(
|
|
40
|
+
`Unknown layer "${this.layer}" — one of ${LAYERS.join(", ")}.\n`,
|
|
41
|
+
);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const out: string[] = [];
|
|
47
|
+
for (const layer of LAYERS) {
|
|
48
|
+
if (this.layer !== undefined && this.layer !== layer) continue;
|
|
49
|
+
const names = registry.items
|
|
50
|
+
.filter((item) => item.layer === layer)
|
|
51
|
+
.map((item) => item.name);
|
|
52
|
+
if (names.length === 0) continue;
|
|
53
|
+
out.push("", ` ${layer} (${names.length})`, ` ${names.join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
out.push("");
|
|
56
|
+
process.stdout.write(out.join("\n"));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const _contract: NebulaCommandClass = NebulaListCommand;
|
|
61
|
+
void _contract;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console-command contract, declared locally.
|
|
3
|
+
*
|
|
4
|
+
* nebula must not import `@c9up/ream`. It is a component package whose only
|
|
5
|
+
* peer is aurora, and taking on the framework to describe two commands would
|
|
6
|
+
* be the tail wagging the dog. Atlas reached the same conclusion for its seven
|
|
7
|
+
* migration commands and declares the same shape in its own `contract.ts`;
|
|
8
|
+
* this is that decision, repeated because the reason is the same.
|
|
9
|
+
*
|
|
10
|
+
* What the kernel actually needs is structural: a class with `commandName`,
|
|
11
|
+
* `description` and a `run()`, plus the metadata it reads to parse argv into
|
|
12
|
+
* instance properties. Ream's `@args` / `@flags` decorators build that
|
|
13
|
+
* metadata; these helpers build the identical shape without them.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface CommandOptions {
|
|
17
|
+
/** Boot the application before `run()`. Off — nebula touches only files. */
|
|
18
|
+
startApp?: boolean;
|
|
19
|
+
staysAlive?: boolean;
|
|
20
|
+
allowUnknownFlags?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ArgumentMetaData {
|
|
24
|
+
type: "string" | "spread";
|
|
25
|
+
propertyName: string;
|
|
26
|
+
argumentName: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
required: boolean;
|
|
29
|
+
default?: string | string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FlagMetaData {
|
|
33
|
+
type: "string" | "boolean" | "number" | "array";
|
|
34
|
+
propertyName: string;
|
|
35
|
+
flagName: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
alias: string[];
|
|
38
|
+
default?: string | string[] | number | boolean;
|
|
39
|
+
required: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The static side the kernel reads. */
|
|
43
|
+
export interface NebulaCommandClass {
|
|
44
|
+
new (): { run(): Promise<void> | void };
|
|
45
|
+
commandName: string;
|
|
46
|
+
description: string;
|
|
47
|
+
options?: CommandOptions;
|
|
48
|
+
args?: readonly ArgumentMetaData[];
|
|
49
|
+
flags?: readonly FlagMetaData[];
|
|
50
|
+
help?: string | string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `dryRun` → `dry-run`, matching what the framework's decorators produce. */
|
|
54
|
+
function dashCase(value: string): string {
|
|
55
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function flag(
|
|
59
|
+
propertyName: string,
|
|
60
|
+
type: FlagMetaData["type"],
|
|
61
|
+
options: {
|
|
62
|
+
flagName?: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
alias?: string[];
|
|
65
|
+
default?: FlagMetaData["default"];
|
|
66
|
+
required?: boolean;
|
|
67
|
+
} = {},
|
|
68
|
+
): FlagMetaData {
|
|
69
|
+
return {
|
|
70
|
+
type,
|
|
71
|
+
propertyName,
|
|
72
|
+
flagName: options.flagName ?? dashCase(propertyName),
|
|
73
|
+
description: options.description,
|
|
74
|
+
alias: options.alias ?? [],
|
|
75
|
+
default: options.default,
|
|
76
|
+
required: options.required ?? false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function argument(
|
|
81
|
+
propertyName: string,
|
|
82
|
+
options: {
|
|
83
|
+
type?: ArgumentMetaData["type"];
|
|
84
|
+
argumentName?: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
required?: boolean;
|
|
87
|
+
default?: ArgumentMetaData["default"];
|
|
88
|
+
} = {},
|
|
89
|
+
): ArgumentMetaData {
|
|
90
|
+
return {
|
|
91
|
+
type: options.type ?? "string",
|
|
92
|
+
propertyName,
|
|
93
|
+
argumentName: options.argumentName ?? dashCase(propertyName),
|
|
94
|
+
description: options.description,
|
|
95
|
+
required: options.required ?? options.default === undefined,
|
|
96
|
+
default: options.default,
|
|
97
|
+
};
|
|
98
|
+
}
|