@octanejs/cli 0.0.1 → 0.0.3

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.
@@ -18,6 +18,9 @@
18
18
  * @property {CommandEntry[]} [subcommands]
19
19
  * @property {boolean} [requiresProject] refuse to run outside a package.json,
20
20
  * for commands that write into one
21
+ * @property {boolean} [passthrough] read the tokens after a bare `--` as
22
+ * `input.rest`. Without it they are a usage error rather than a silent drop,
23
+ * on the same reasoning as an unknown flag
21
24
  * @property {(ctx: import('./context.js').Ctx, input: CommandInput) => Promise<CommandResult>} [run]
22
25
  */
23
26
 
@@ -8,6 +8,13 @@ const MANAGERS = {
8
8
  bun: { add: ['add'], dev: '-d' },
9
9
  };
10
10
 
11
+ /**
12
+ * The managers this CLI knows how to drive. Derived rather than written out, so
13
+ * that a flag can never offer a manager `installCommand` would silently answer
14
+ * with npm's spelling.
15
+ */
16
+ export const PACKAGE_MANAGERS = Object.keys(MANAGERS);
17
+
11
18
  /**
12
19
  * @param {import('./project.js').Project} project
13
20
  * @param {string[]} names
@@ -109,6 +109,20 @@ async function dispatch(argv, options) {
109
109
  return EXIT.OK;
110
110
  }
111
111
 
112
+ // A command that has not asked for `rest` would drop these on the floor. That
113
+ // is the same failure the parser refuses for an unknown flag, and it arrives
114
+ // here by an ordinary route: npm eats the `--` and forwards what follows, so
115
+ // `npm create octane app -- --template spa` is correct and the identical line
116
+ // under pnpm or yarn hands the `--` straight through. Dropped silently, the
117
+ // run then fails saying --template is required, naming the one flag that was
118
+ // in fact supplied.
119
+ if (parsed.rest.length > 0 && !module.passthrough) {
120
+ throw usageError(
121
+ `Unexpected arguments after --: ${parsed.rest.join(' ')}`,
122
+ 'npm needs `--` to forward flags. pnpm and yarn forward them already, so drop it there.',
123
+ );
124
+ }
125
+
112
126
  // Commands that write into a project need one to exist. Without this they
113
127
  // fail deep inside an fs call with a raw ENOENT stack.
114
128
  if (module.requiresProject && ctx.project().manifestPath === null) {
@@ -9,6 +9,11 @@
9
9
  * @type {import('./command.js').CommandEntry[]}
10
10
  */
11
11
  export const COMMANDS = [
12
+ {
13
+ name: 'create',
14
+ summary: 'Create an Octane app in a new directory.',
15
+ load: () => import('../commands/create.js'),
16
+ },
12
17
  {
13
18
  name: 'init',
14
19
  summary: 'Wire Octane into the project in this directory.',
package/src/kernel/ui.js CHANGED
@@ -60,6 +60,20 @@ export function createUi({ mode, yes = false, stdout = process.stdout }) {
60
60
  );
61
61
  };
62
62
 
63
+ /**
64
+ * Whether `--yes` already answers this question.
65
+ *
66
+ * It means "stop asking me", which is as true on a terminal as it is in a
67
+ * pipe. Consulting it only once the CLI had decided nobody was watching made
68
+ * the flag do nothing in the one place people type it by hand, and left a
69
+ * caller wanting an unattended run no choice but to claim there was no
70
+ * terminal, which costs the interactive rendering with it. A question with no
71
+ * default has nothing to answer with, so it is still asked.
72
+ *
73
+ * @param {unknown} initial
74
+ */
75
+ const answered = (initial) => yes && initial !== undefined;
76
+
63
77
  /** @param {unknown} value */
64
78
  const guard = (value) => {
65
79
  if (clack.isCancel(value)) throw new CliError('Cancelled.');
@@ -124,15 +138,36 @@ export function createUi({ mode, yes = false, stdout = process.stdout }) {
124
138
  * @returns {Promise<string>}
125
139
  */
126
140
  async select({ message, flag, options, initial }) {
141
+ if (answered(initial)) return String(initial);
127
142
  if (mode !== 'interactive') return String(unattended(flag, initial));
128
143
  return String(guard(await clack.select({ message, options, initialValue: initial })));
129
144
  },
130
145
 
146
+ /**
147
+ * A free-text answer. `initial` is what an empty submission means, not
148
+ * pre-filled input: someone accepting the offer by pressing enter and
149
+ * someone typing it out should land in the same place.
150
+ *
151
+ * @param {{ message: string, flag: string, placeholder?: string, initial?: string }} options
152
+ * @returns {Promise<string>}
153
+ */
154
+ async text({ message, flag, placeholder, initial }) {
155
+ if (answered(initial)) return String(initial);
156
+ if (mode !== 'interactive') return String(unattended(flag, initial));
157
+ const answer = String(
158
+ guard(await clack.text({ message, placeholder, defaultValue: initial })),
159
+ );
160
+ return answer.trim() === '' && initial !== undefined ? initial : answer.trim();
161
+ },
162
+
131
163
  /**
132
164
  * @param {{ message: string, flag: string, options: Choice[], initial?: string[] }} options
133
165
  * @returns {Promise<string[]>}
134
166
  */
135
167
  async multiselect({ message, flag, options, initial }) {
168
+ // `answered` is a boolean, not a type guard, and this is the one prompt
169
+ // that hands its default straight back rather than coercing it.
170
+ if (answered(initial)) return /** @type {string[]} */ (initial);
136
171
  if (mode !== 'interactive') {
137
172
  return /** @type {string[]} */ (unattended(flag, initial));
138
173
  }
@@ -146,6 +181,7 @@ export function createUi({ mode, yes = false, stdout = process.stdout }) {
146
181
  * @returns {Promise<boolean>}
147
182
  */
148
183
  async confirm({ message, flag, initial = true }) {
184
+ if (answered(initial)) return Boolean(initial);
149
185
  if (mode !== 'interactive') return Boolean(unattended(flag, initial));
150
186
  return Boolean(guard(await clack.confirm({ message, initialValue: initial })));
151
187
  },