@hs-x/cli 0.1.3 → 0.2.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/dist/account-store.d.ts +45 -22
- package/dist/account-store.d.ts.map +1 -1
- package/dist/account-store.js +73 -16
- package/dist/account-store.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +170 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/kit.d.ts +1 -1
- package/dist/cli/kit.d.ts.map +1 -1
- package/dist/cli/kit.js.map +1 -1
- package/dist/commands/account.d.ts +6 -6
- package/dist/commands/account.d.ts.map +1 -1
- package/dist/commands/account.js +25 -23
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/api.d.ts +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +6 -4
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/connect.d.ts +16 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +143 -17
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/control-plane-read.d.ts +3 -3
- package/dist/commands/control-plane-read.d.ts.map +1 -1
- package/dist/commands/control-plane-read.js +21 -33
- package/dist/commands/control-plane-read.js.map +1 -1
- package/dist/commands/deploy-promote.d.ts +1 -1
- package/dist/commands/deploy-promote.d.ts.map +1 -1
- package/dist/commands/deploy-promote.js +3 -3
- package/dist/commands/deploy-promote.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +39 -2
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +11 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/flags.d.ts +1 -1
- package/dist/commands/flags.d.ts.map +1 -1
- package/dist/commands/flags.js +59 -35
- package/dist/commands/flags.js.map +1 -1
- package/dist/commands/help-command.d.ts +14 -0
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +274 -20
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/init.d.ts +11 -8
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +435 -246
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +1 -26
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +11 -236
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/login.d.ts +4 -12
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +196 -390
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/migrate.d.ts +2 -7
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +84 -201
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/rollback.d.ts +1 -20
- package/dist/commands/rollback.d.ts.map +1 -1
- package/dist/commands/rollback.js +6 -275
- package/dist/commands/rollback.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unlink.d.ts +1 -21
- package/dist/commands/unlink.d.ts.map +1 -1
- package/dist/commands/unlink.js +12 -94
- package/dist/commands/unlink.js.map +1 -1
- package/dist/commands/update.js +7 -7
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/validate.d.ts +2 -2
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +1 -1
- package/dist/commands/validate.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -4
- package/dist/constants.js.map +1 -1
- package/dist/control-plane-fetch.d.ts.map +1 -1
- package/dist/control-plane-fetch.js +2 -2
- package/dist/control-plane-fetch.js.map +1 -1
- package/dist/control-plane-loader.d.ts +1 -1
- package/dist/control-plane-loader.js +1 -1
- package/dist/errors-registry.js +1 -1
- package/dist/errors-registry.js.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +3 -2
- package/dist/help.js.map +1 -1
- package/dist/init/templates.d.ts +11 -3
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +94 -25
- package/dist/init/templates.js.map +1 -1
- package/dist/project-binding.d.ts +17 -0
- package/dist/project-binding.d.ts.map +1 -0
- package/dist/project-binding.js +30 -0
- package/dist/project-binding.js.map +1 -0
- package/dist/prompt.d.ts +21 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +186 -50
- package/dist/prompt.js.map +1 -1
- package/dist/reporter/human.d.ts +5 -0
- package/dist/reporter/human.d.ts.map +1 -1
- package/dist/reporter/human.js +48 -18
- package/dist/reporter/human.js.map +1 -1
- package/dist/reporter/json.d.ts +6 -0
- package/dist/reporter/json.d.ts.map +1 -1
- package/dist/reporter/json.js +16 -0
- package/dist/reporter/json.js.map +1 -1
- package/dist/reporter/style.d.ts +1 -1
- package/dist/reporter/style.d.ts.map +1 -1
- package/dist/reporter/style.js +10 -3
- package/dist/reporter/style.js.map +1 -1
- package/dist/reporter/types.d.ts +9 -0
- package/dist/reporter/types.d.ts.map +1 -1
- package/dist/reporter/types.js.map +1 -1
- package/dist/services/account-store.d.ts +9 -4
- package/dist/services/account-store.d.ts.map +1 -1
- package/dist/services/account-store.js +46 -22
- package/dist/services/account-store.js.map +1 -1
- package/dist/services/control-plane.js +3 -2
- package/dist/services/control-plane.js.map +1 -1
- package/dist/services/live.d.ts +1 -1
- package/dist/services/reporter.d.ts +3 -0
- package/dist/services/reporter.d.ts.map +1 -1
- package/dist/services/reporter.js +2 -0
- package/dist/services/reporter.js.map +1 -1
- package/package.json +11 -8
package/dist/commands/init.js
CHANGED
|
@@ -1,176 +1,15 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { mkdir, stat, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
2
|
import * as Args from '@effect/cli/Args';
|
|
5
3
|
import * as Command from '@effect/cli/Command';
|
|
6
4
|
import * as Options from '@effect/cli/Options';
|
|
5
|
+
import { FileSystem, Path } from '@effect/platform';
|
|
7
6
|
import { Effect, Option } from 'effect';
|
|
8
|
-
import { exitWith } from '../cli-error.js';
|
|
9
|
-
import { cwdOption, jsonOption, runHandler
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
7
|
+
import { cliError, exitWith } from '../cli-error.js';
|
|
8
|
+
import { cwdOption, jsonOption, runHandler } from '../cli/kit.js';
|
|
9
|
+
import { getTemplate, resolveTemplateType, TEMPLATES, } from '../init/templates.js';
|
|
10
|
+
import { BACK, isInteractive, promptConfirm, promptSelect, promptText } from '../prompt.js';
|
|
12
11
|
import { Cwd } from '../services/cwd.js';
|
|
13
|
-
|
|
14
|
-
const { getTemplate, resolveTemplateType, TEMPLATES } = await import('../init/templates.js');
|
|
15
|
-
const positional = argv[1] && !argv[1].startsWith('-') ? argv[1] : undefined;
|
|
16
|
-
const flagName = resolveFlag(argv, '--name');
|
|
17
|
-
const flagTemplateRaw = resolveFlag(argv, '--template') ?? resolveFlag(argv, '--type');
|
|
18
|
-
const flagTemplate = resolveTemplateType(flagTemplateRaw);
|
|
19
|
-
if (flagTemplateRaw && !flagTemplate) {
|
|
20
|
-
throw new Error(`Unknown app type: ${flagTemplateRaw}. Use workflow-action, sync-source, or empty.`);
|
|
21
|
-
}
|
|
22
|
-
const flagAppName = resolveFlag(argv, '--app-name') ?? resolveFlag(argv, '--display-name');
|
|
23
|
-
const flagYes = argv.includes('--yes') || argv.includes('-y');
|
|
24
|
-
const flagSkipInstall = argv.includes('--no-install');
|
|
25
|
-
const force = argv.includes('--force');
|
|
26
|
-
const interactive = !json && isInteractive();
|
|
27
|
-
let workingCwd = cwd;
|
|
28
|
-
if (positional &&
|
|
29
|
-
(positional.includes('/') || positional.startsWith('.') || positional.startsWith('~'))) {
|
|
30
|
-
const expanded = positional.startsWith('~')
|
|
31
|
-
? join(process.env.HOME ?? '', positional.slice(1))
|
|
32
|
-
: positional;
|
|
33
|
-
const abs = resolve(cwd, expanded);
|
|
34
|
-
workingCwd = dirname(abs);
|
|
35
|
-
}
|
|
36
|
-
const reporter = createReporter({ command: 'init', argv, entry: true });
|
|
37
|
-
let bannerShown = false;
|
|
38
|
-
if (interactive) {
|
|
39
|
-
reporter.banner();
|
|
40
|
-
bannerShown = true;
|
|
41
|
-
}
|
|
42
|
-
const rawName = positional ?? flagName;
|
|
43
|
-
let name;
|
|
44
|
-
if (rawName) {
|
|
45
|
-
name = basename(rawName.startsWith('~')
|
|
46
|
-
? join(process.env.HOME ?? '', rawName.slice(1))
|
|
47
|
-
: resolve(workingCwd, rawName));
|
|
48
|
-
const validationError = validateProjectName(name);
|
|
49
|
-
if (validationError) {
|
|
50
|
-
const code = 'HSX_E_INPUT_BAD_NAME';
|
|
51
|
-
reporter.error(code, `Invalid project name: ${name}`, {
|
|
52
|
-
hint: validationError,
|
|
53
|
-
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
54
|
-
});
|
|
55
|
-
reporter.done(undefined, 10);
|
|
56
|
-
return { exitCode: 10 };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (!name) {
|
|
60
|
-
if (interactive) {
|
|
61
|
-
const answer = await promptText({
|
|
62
|
-
message: 'Project name',
|
|
63
|
-
default: 'hsx-app',
|
|
64
|
-
validate: validateProjectName,
|
|
65
|
-
});
|
|
66
|
-
if (answer === undefined)
|
|
67
|
-
return cancelledResult(argv, 'init');
|
|
68
|
-
name = answer;
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
name = 'hsx-app';
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
let templateId = flagTemplate ?? 'deals-tagger';
|
|
75
|
-
if (!flagTemplate && interactive) {
|
|
76
|
-
const picked = await promptSelect({
|
|
77
|
-
message: 'What are you building?',
|
|
78
|
-
default: 'deals-tagger',
|
|
79
|
-
options: TEMPLATES.map((t) => ({
|
|
80
|
-
value: t.id,
|
|
81
|
-
label: t.label,
|
|
82
|
-
description: t.description,
|
|
83
|
-
})),
|
|
84
|
-
});
|
|
85
|
-
if (picked === undefined)
|
|
86
|
-
return cancelledResult(argv, 'init');
|
|
87
|
-
templateId = picked;
|
|
88
|
-
}
|
|
89
|
-
let appName = flagAppName;
|
|
90
|
-
if (appName === undefined && interactive) {
|
|
91
|
-
const answer = await promptText({
|
|
92
|
-
message: 'HubSpot app name',
|
|
93
|
-
default: toDisplayName(name),
|
|
94
|
-
validate(value) {
|
|
95
|
-
return value.trim().length === 0 ? 'Enter an app name.' : undefined;
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
if (answer === undefined)
|
|
99
|
-
return cancelledResult(argv, 'init');
|
|
100
|
-
appName = answer.trim();
|
|
101
|
-
}
|
|
102
|
-
appName = appName?.trim() || toDisplayName(name);
|
|
103
|
-
let shouldInstall = false;
|
|
104
|
-
if (!flagSkipInstall && interactive && !flagYes) {
|
|
105
|
-
const confirmed = await promptConfirm({
|
|
106
|
-
message: 'Install dependencies now?',
|
|
107
|
-
default: true,
|
|
108
|
-
});
|
|
109
|
-
if (confirmed === undefined)
|
|
110
|
-
return cancelledResult(argv, 'init');
|
|
111
|
-
shouldInstall = confirmed;
|
|
112
|
-
}
|
|
113
|
-
else if (flagYes && !flagSkipInstall) {
|
|
114
|
-
shouldInstall = true;
|
|
115
|
-
}
|
|
116
|
-
const root = resolve(workingCwd, name);
|
|
117
|
-
const configPath = join(root, 'hsx.config.ts');
|
|
118
|
-
const configExists = await stat(configPath).then(() => true, () => false);
|
|
119
|
-
if (configExists && !force) {
|
|
120
|
-
const code = 'HSX_E_INIT_DIR_NOT_EMPTY';
|
|
121
|
-
reporter.error(code, `Project already exists at ${root}.`, {
|
|
122
|
-
hint: `Pick a new directory, delete the existing one (rm -rf ${root}), or pass --force to overwrite.`,
|
|
123
|
-
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
124
|
-
});
|
|
125
|
-
reporter.done(undefined, 10);
|
|
126
|
-
return { exitCode: 10 };
|
|
127
|
-
}
|
|
128
|
-
if (interactive)
|
|
129
|
-
reporter.header(name);
|
|
130
|
-
else if (!json && !bannerShown) {
|
|
131
|
-
reporter.banner();
|
|
132
|
-
reporter.header(name);
|
|
133
|
-
}
|
|
134
|
-
const template = getTemplate(templateId);
|
|
135
|
-
const files = template.files(name, { appName });
|
|
136
|
-
await mkdir(join(root, 'src', 'workers'), { recursive: true });
|
|
137
|
-
const scaffoldStep = reporter.step('Scaffolding project');
|
|
138
|
-
for (const file of files) {
|
|
139
|
-
const filePath = join(root, file.path);
|
|
140
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
141
|
-
await writeFile(filePath, file.contents, { flag: force ? 'w' : 'wx' });
|
|
142
|
-
}
|
|
143
|
-
scaffoldStep.ok(`${files.length} file${files.length === 1 ? '' : 's'} ${root}`);
|
|
144
|
-
if (shouldInstall) {
|
|
145
|
-
const installStep = reporter.step('Installing dependencies');
|
|
146
|
-
try {
|
|
147
|
-
await runInstall(root);
|
|
148
|
-
installStep.ok();
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
152
|
-
installStep.fail(message);
|
|
153
|
-
reporter.warn('HSX_W_INIT_INSTALL_FAILED', `Dependency install failed; you can retry with: cd ${name} && bun install`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const result = {
|
|
157
|
-
ok: true,
|
|
158
|
-
command: 'init',
|
|
159
|
-
project: { name, appName, root, template: templateId, installed: shouldInstall },
|
|
160
|
-
};
|
|
161
|
-
if (json) {
|
|
162
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
163
|
-
return { exitCode: 0 };
|
|
164
|
-
}
|
|
165
|
-
reporter.info('');
|
|
166
|
-
reporter.info('Next steps:');
|
|
167
|
-
reporter.info(` cd ${name}`);
|
|
168
|
-
if (!shouldInstall)
|
|
169
|
-
reporter.info(' bun install');
|
|
170
|
-
reporter.info(' hs-x dev');
|
|
171
|
-
reporter.done('Created');
|
|
172
|
-
return { exitCode: 0 };
|
|
173
|
-
}
|
|
12
|
+
import { Reporter } from '../services/reporter.js';
|
|
174
13
|
function validateProjectName(input) {
|
|
175
14
|
if (!input)
|
|
176
15
|
return 'Name is required.';
|
|
@@ -185,53 +24,66 @@ function toDisplayName(name) {
|
|
|
185
24
|
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
|
|
186
25
|
.join(' ');
|
|
187
26
|
}
|
|
27
|
+
/** Resolve an optional enum flag against its allowed values (trimmed, lowercased). */
|
|
28
|
+
function resolveEnumFlag(value, allowed) {
|
|
29
|
+
const raw = Option.getOrUndefined(value)?.trim().toLowerCase();
|
|
30
|
+
return raw && allowed.includes(raw) ? raw : undefined;
|
|
31
|
+
}
|
|
32
|
+
// LEAF QUARANTINE: genuine subprocess I/O. Spawns `bun install` in the new
|
|
33
|
+
// project root; wrapped in `Effect.tryPromise` by the handler.
|
|
34
|
+
//
|
|
35
|
+
// A timeout is mandatory: `bun install` retries network fetches with backoff
|
|
36
|
+
// when the registry is unreachable, so an offline scaffold would otherwise sit
|
|
37
|
+
// on a silent (stdio: 'ignore') `… Installing dependencies` line forever. On
|
|
38
|
+
// timeout we kill the child and reject; the handler then surfaces the graceful
|
|
39
|
+
// "retry with bun install" hint instead of hanging.
|
|
40
|
+
const INSTALL_TIMEOUT_MS = 180_000;
|
|
188
41
|
async function runInstall(root) {
|
|
42
|
+
try {
|
|
43
|
+
await runInstallWith('bun', root);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// The CLI runs fine under plain node; a node-only machine has no `bun` on
|
|
47
|
+
// PATH (spawn ENOENT). Fall back to npm so `npm i -g @hs-x/cli && hs-x init`
|
|
48
|
+
// works without installing Bun first.
|
|
49
|
+
if (error.code === 'ENOENT') {
|
|
50
|
+
await runInstallWith('npm', root);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function runInstallWith(tool, root) {
|
|
189
57
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
190
|
-
const child = spawn(
|
|
191
|
-
|
|
58
|
+
const child = spawn(tool, ['install'], { cwd: root, stdio: 'ignore' });
|
|
59
|
+
const timer = setTimeout(() => {
|
|
60
|
+
child.kill('SIGKILL');
|
|
61
|
+
rejectPromise(new Error(`${tool} install timed out after ${INSTALL_TIMEOUT_MS / 1000}s (registry unreachable?)`));
|
|
62
|
+
}, INSTALL_TIMEOUT_MS);
|
|
63
|
+
timer.unref?.();
|
|
64
|
+
child.on('error', (error) => {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
rejectPromise(error);
|
|
67
|
+
});
|
|
192
68
|
child.on('exit', (code) => {
|
|
69
|
+
clearTimeout(timer);
|
|
193
70
|
if (code === 0)
|
|
194
71
|
resolvePromise();
|
|
195
72
|
else
|
|
196
|
-
rejectPromise(new Error(
|
|
73
|
+
rejectPromise(new Error(`${tool} install exited with code ${code}`));
|
|
197
74
|
});
|
|
198
75
|
});
|
|
199
76
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
function resolveFlag(argv, flag) {
|
|
209
|
-
const index = argv.indexOf(flag);
|
|
210
|
-
if (index !== -1) {
|
|
211
|
-
const next = argv[index + 1];
|
|
212
|
-
if (isFlagValue(next))
|
|
213
|
-
return next;
|
|
214
|
-
}
|
|
215
|
-
const prefix = `${flag}=`;
|
|
216
|
-
const found = argv.find((arg) => arg.startsWith(prefix));
|
|
217
|
-
if (!found)
|
|
218
|
-
return undefined;
|
|
219
|
-
const value = found.slice(prefix.length);
|
|
220
|
-
return value.length > 0 ? value : undefined;
|
|
221
|
-
}
|
|
222
|
-
// ─── Native @effect/cli command (Phase 4) ──────────────────────────────────
|
|
223
|
-
// `init` is QUARANTINED: scaffolding binds to an imperative, prompt-driven body
|
|
224
|
-
// (`promptText`/`promptSelect`/`promptConfirm`, `mkdir`/`writeFile`, and a
|
|
225
|
-
// `bun install` child process) whose internal purity is out of scope for this
|
|
226
|
-
// pass. The native command reconstructs the legacy argv from its typed options /
|
|
227
|
-
// positional and delegates to the existing `initProject` Promise via
|
|
228
|
-
// `Effect.tryPromise`; only the command boundary is Effect. A non-zero
|
|
229
|
-
// `CliResult.exitCode` is surfaced as a silent `exitWith(code)` (output already
|
|
230
|
-
// rendered by the legacy body) so `runHandler` does not re-render.
|
|
77
|
+
// ─── Native @effect/cli command ────────────────────────────────────────────
|
|
78
|
+
// `init` is de-quarantined: the scaffolding logic runs directly on Effect
|
|
79
|
+
// services (`FileSystem`/`Path` for all fs, `Reporter` for all output, `Cwd`
|
|
80
|
+
// for the working dir). Only two genuine leaf I/O seams remain wrapped in
|
|
81
|
+
// `Effect.tryPromise`/`Effect.promise`: the interactive TTY prompts
|
|
82
|
+
// (`promptText`/`promptSelect`/`promptConfirm`, non-json TTY only) and the
|
|
83
|
+
// `bun install` subprocess (`runInstall`, only when the user opts in).
|
|
231
84
|
//
|
|
232
85
|
// `init` and its `create` alias are built from one factory; only the command
|
|
233
|
-
// name differs
|
|
234
|
-
// parses).
|
|
86
|
+
// name differs.
|
|
235
87
|
const nameArg = Args.text({ name: 'name' }).pipe(Args.optional);
|
|
236
88
|
// Legacy init accepts the project name as EITHER a positional or `--name`.
|
|
237
89
|
const nameOption = Options.text('name').pipe(Options.optional);
|
|
@@ -239,9 +91,376 @@ const templateOption = Options.text('template').pipe(Options.optional);
|
|
|
239
91
|
const typeOption = Options.text('type').pipe(Options.optional);
|
|
240
92
|
const appNameOption = Options.text('app-name').pipe(Options.optional);
|
|
241
93
|
const displayNameOption = Options.text('display-name').pipe(Options.optional);
|
|
94
|
+
const distributionOption = Options.text('distribution').pipe(Options.optional);
|
|
95
|
+
const authOption = Options.text('auth').pipe(Options.optional);
|
|
96
|
+
// Two explicit flags give the tri-state (@effect/cli doesn't auto-negate):
|
|
97
|
+
// `--ui-extension` forces on, `--no-ui-extension` forces off, neither prompts.
|
|
98
|
+
const uiExtensionOption = Options.boolean('ui-extension').pipe(Options.withDefault(false));
|
|
99
|
+
const noUiExtensionOption = Options.boolean('no-ui-extension').pipe(Options.withDefault(false));
|
|
242
100
|
const yesOption = Options.boolean('yes').pipe(Options.withAlias('y'), Options.withDefault(false));
|
|
243
101
|
const noInstallOption = Options.boolean('no-install').pipe(Options.withDefault(false));
|
|
244
102
|
const forceOption = Options.boolean('force').pipe(Options.withDefault(false));
|
|
103
|
+
/** Emit a cancelled result (interactive prompt aborted). */
|
|
104
|
+
function cancelled() {
|
|
105
|
+
return Effect.gen(function* () {
|
|
106
|
+
const reporter = yield* Reporter;
|
|
107
|
+
yield* reporter.info('Cancelled.');
|
|
108
|
+
return yield* Effect.fail(exitWith(130));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function initBody(opts) {
|
|
112
|
+
return Effect.gen(function* () {
|
|
113
|
+
const fs = yield* FileSystem.FileSystem;
|
|
114
|
+
const pathSvc = yield* Path.Path;
|
|
115
|
+
const reporter = yield* Reporter;
|
|
116
|
+
// `@effect/cli` greedily consumes the NEXT token as a text option's value,
|
|
117
|
+
// so `--name --json` yields nameFlag="--json". Treat a flag-like value as a
|
|
118
|
+
// missing name and restore the swallowed `--json` so json mode isn't lost.
|
|
119
|
+
let json = opts.json;
|
|
120
|
+
let flagName;
|
|
121
|
+
if (Option.isSome(opts.nameFlag)) {
|
|
122
|
+
const value = opts.nameFlag.value;
|
|
123
|
+
if (value.startsWith('-')) {
|
|
124
|
+
if (value === '--json')
|
|
125
|
+
json = true;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
flagName = value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const positional = Option.getOrUndefined(opts.name);
|
|
132
|
+
const flagTemplateRaw = Option.getOrUndefined(opts.template) ?? Option.getOrUndefined(opts.type);
|
|
133
|
+
const flagTemplate = resolveTemplateType(flagTemplateRaw);
|
|
134
|
+
if (flagTemplateRaw && !flagTemplate) {
|
|
135
|
+
// Surface the same flat `{ok:false,error}` / `Error: …` shape the legacy
|
|
136
|
+
// direct dispatcher produced for a thrown error — rendered here (not via
|
|
137
|
+
// the registry error block) so the exit code path stays byte-identical.
|
|
138
|
+
const message = `Unknown app type: ${flagTemplateRaw}. Use workflow-action, sync-source, or empty.`;
|
|
139
|
+
if (json) {
|
|
140
|
+
yield* reporter.raw(`${JSON.stringify({ ok: false, error: message }, null, 2)}\n`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
yield* reporter.rawErr(`Error: ${message}\n`);
|
|
144
|
+
}
|
|
145
|
+
return yield* Effect.fail(exitWith(1));
|
|
146
|
+
}
|
|
147
|
+
const flagAppName = Option.getOrUndefined(opts.appName) ?? Option.getOrUndefined(opts.displayName);
|
|
148
|
+
const flagYes = opts.yes;
|
|
149
|
+
const flagSkipInstall = opts.noInstall;
|
|
150
|
+
const force = opts.force;
|
|
151
|
+
const interactive = !json && isInteractive();
|
|
152
|
+
// `--cwd` overrides the injected process working dir.
|
|
153
|
+
let workingCwd = yield* Option.match(opts.cwd, {
|
|
154
|
+
onNone: () => Cwd,
|
|
155
|
+
onSome: (dir) => Effect.succeed(dir),
|
|
156
|
+
});
|
|
157
|
+
if (positional &&
|
|
158
|
+
(positional.includes('/') || positional.startsWith('.') || positional.startsWith('~'))) {
|
|
159
|
+
const expanded = positional.startsWith('~')
|
|
160
|
+
? pathSvc.join(process.env.HOME ?? '', positional.slice(1))
|
|
161
|
+
: positional;
|
|
162
|
+
const abs = pathSvc.resolve(workingCwd, expanded);
|
|
163
|
+
workingCwd = pathSvc.dirname(abs);
|
|
164
|
+
}
|
|
165
|
+
let bannerShown = false;
|
|
166
|
+
if (interactive) {
|
|
167
|
+
yield* reporter.banner;
|
|
168
|
+
bannerShown = true;
|
|
169
|
+
}
|
|
170
|
+
const rawName = positional ?? flagName;
|
|
171
|
+
let name;
|
|
172
|
+
if (rawName) {
|
|
173
|
+
name = pathSvc.basename(rawName.startsWith('~')
|
|
174
|
+
? pathSvc.join(process.env.HOME ?? '', rawName.slice(1))
|
|
175
|
+
: pathSvc.resolve(workingCwd, rawName));
|
|
176
|
+
const validationError = validateProjectName(name);
|
|
177
|
+
if (validationError) {
|
|
178
|
+
const code = 'HSX_E_INPUT_BAD_NAME';
|
|
179
|
+
yield* reporter.error(code, `Invalid project name: ${name}`, {
|
|
180
|
+
hint: validationError,
|
|
181
|
+
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
182
|
+
});
|
|
183
|
+
yield* reporter.done(undefined, 10);
|
|
184
|
+
return yield* Effect.fail(exitWith(10));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Enum flags are validated up front (the wizard never sees a bad value).
|
|
188
|
+
if (Option.isSome(opts.distribution) && !resolveEnumFlag(opts.distribution, ['private', 'marketplace'])) {
|
|
189
|
+
const message = `Unknown distribution: ${opts.distribution.value}. Use private or marketplace.`;
|
|
190
|
+
if (json)
|
|
191
|
+
yield* reporter.raw(`${JSON.stringify({ ok: false, error: message }, null, 2)}\n`);
|
|
192
|
+
else
|
|
193
|
+
yield* reporter.rawErr(`Error: ${message}\n`);
|
|
194
|
+
return yield* Effect.fail(exitWith(1));
|
|
195
|
+
}
|
|
196
|
+
if (Option.isSome(opts.auth) && !resolveEnumFlag(opts.auth, ['oauth', 'static'])) {
|
|
197
|
+
const message = `Unknown auth: ${opts.auth.value}. Use oauth or static.`;
|
|
198
|
+
if (json)
|
|
199
|
+
yield* reporter.raw(`${JSON.stringify({ ok: false, error: message }, null, 2)}\n`);
|
|
200
|
+
else
|
|
201
|
+
yield* reporter.rawErr(`Error: ${message}\n`);
|
|
202
|
+
return yield* Effect.fail(exitWith(1));
|
|
203
|
+
}
|
|
204
|
+
const flagDistribution = resolveEnumFlag(opts.distribution, ['private', 'marketplace']);
|
|
205
|
+
const flagAuth = resolveEnumFlag(opts.auth, ['oauth', 'static']);
|
|
206
|
+
// Wizard state. Flags pre-seed answers (those steps are skipped); --yes or
|
|
207
|
+
// a non-interactive run accepts the defaults below as-is.
|
|
208
|
+
const draft = {
|
|
209
|
+
name: name ?? 'hsx-app',
|
|
210
|
+
templateId: flagTemplate ?? 'deals-tagger',
|
|
211
|
+
uiExtension: opts.uiExtension && !opts.noUiExtension,
|
|
212
|
+
distribution: flagDistribution ?? 'private',
|
|
213
|
+
auth: flagAuth ?? 'oauth',
|
|
214
|
+
appName: flagAppName,
|
|
215
|
+
shouldInstall: !flagSkipInstall,
|
|
216
|
+
};
|
|
217
|
+
if (interactive && !flagYes) {
|
|
218
|
+
const steps = [
|
|
219
|
+
{
|
|
220
|
+
skip: () => name !== undefined,
|
|
221
|
+
run: async () => {
|
|
222
|
+
const answer = await promptText({
|
|
223
|
+
message: 'Project name',
|
|
224
|
+
default: draft.name,
|
|
225
|
+
validate: validateProjectName,
|
|
226
|
+
allowBack: true,
|
|
227
|
+
});
|
|
228
|
+
if (answer === undefined)
|
|
229
|
+
return 'cancel';
|
|
230
|
+
if (answer === BACK)
|
|
231
|
+
return 'back';
|
|
232
|
+
draft.name = answer;
|
|
233
|
+
return 'next';
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
skip: () => flagTemplate !== undefined,
|
|
238
|
+
run: async () => {
|
|
239
|
+
const picked = await promptSelect({
|
|
240
|
+
message: 'What are you building?',
|
|
241
|
+
default: draft.templateId,
|
|
242
|
+
options: TEMPLATES.map((t) => ({
|
|
243
|
+
value: t.id,
|
|
244
|
+
label: t.label,
|
|
245
|
+
description: t.description,
|
|
246
|
+
})),
|
|
247
|
+
allowBack: true,
|
|
248
|
+
});
|
|
249
|
+
if (picked === undefined)
|
|
250
|
+
return 'cancel';
|
|
251
|
+
if (picked === BACK)
|
|
252
|
+
return 'back';
|
|
253
|
+
draft.templateId = picked;
|
|
254
|
+
return 'next';
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
// UI extension (app card): --ui-extension → on; --no-ui-extension → off.
|
|
259
|
+
skip: () => opts.uiExtension || opts.noUiExtension,
|
|
260
|
+
run: async () => {
|
|
261
|
+
const confirmed = await promptConfirm({
|
|
262
|
+
message: 'Add a UI extension (app card)?',
|
|
263
|
+
default: draft.uiExtension,
|
|
264
|
+
allowBack: true,
|
|
265
|
+
});
|
|
266
|
+
if (confirmed === undefined)
|
|
267
|
+
return 'cancel';
|
|
268
|
+
if (confirmed === BACK)
|
|
269
|
+
return 'back';
|
|
270
|
+
draft.uiExtension = confirmed;
|
|
271
|
+
return 'next';
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
skip: () => flagDistribution !== undefined,
|
|
276
|
+
run: async () => {
|
|
277
|
+
const picked = await promptSelect({
|
|
278
|
+
message: 'How will this app be distributed?',
|
|
279
|
+
default: draft.distribution,
|
|
280
|
+
options: [
|
|
281
|
+
{ value: 'private', label: 'Private', description: 'One account; not listed publicly' },
|
|
282
|
+
{
|
|
283
|
+
value: 'marketplace',
|
|
284
|
+
label: 'Marketplace',
|
|
285
|
+
description: 'Listed on the HubSpot App Marketplace',
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
allowBack: true,
|
|
289
|
+
});
|
|
290
|
+
if (picked === undefined)
|
|
291
|
+
return 'cancel';
|
|
292
|
+
if (picked === BACK)
|
|
293
|
+
return 'back';
|
|
294
|
+
draft.distribution = picked;
|
|
295
|
+
return 'next';
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
skip: () => flagAuth !== undefined,
|
|
300
|
+
run: async () => {
|
|
301
|
+
const picked = await promptSelect({
|
|
302
|
+
message: 'How does the app authenticate?',
|
|
303
|
+
default: draft.auth,
|
|
304
|
+
options: [
|
|
305
|
+
{ value: 'oauth', label: 'OAuth', description: 'Install-time OAuth; per-portal tokens' },
|
|
306
|
+
{ value: 'static', label: 'Static', description: 'A single static access token' },
|
|
307
|
+
],
|
|
308
|
+
allowBack: true,
|
|
309
|
+
});
|
|
310
|
+
if (picked === undefined)
|
|
311
|
+
return 'cancel';
|
|
312
|
+
if (picked === BACK)
|
|
313
|
+
return 'back';
|
|
314
|
+
draft.auth = picked;
|
|
315
|
+
return 'next';
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
skip: () => flagAppName !== undefined,
|
|
320
|
+
run: async () => {
|
|
321
|
+
const answer = await promptText({
|
|
322
|
+
message: 'HubSpot app name',
|
|
323
|
+
default: draft.appName ?? toDisplayName(draft.name),
|
|
324
|
+
validate(value) {
|
|
325
|
+
return value.trim().length === 0 ? 'Enter an app name.' : undefined;
|
|
326
|
+
},
|
|
327
|
+
allowBack: true,
|
|
328
|
+
});
|
|
329
|
+
if (answer === undefined)
|
|
330
|
+
return 'cancel';
|
|
331
|
+
if (answer === BACK)
|
|
332
|
+
return 'back';
|
|
333
|
+
draft.appName = answer.trim();
|
|
334
|
+
return 'next';
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
skip: () => flagSkipInstall,
|
|
339
|
+
run: async () => {
|
|
340
|
+
const confirmed = await promptConfirm({
|
|
341
|
+
message: 'Install dependencies now?',
|
|
342
|
+
default: draft.shouldInstall,
|
|
343
|
+
allowBack: true,
|
|
344
|
+
});
|
|
345
|
+
if (confirmed === undefined)
|
|
346
|
+
return 'cancel';
|
|
347
|
+
if (confirmed === BACK)
|
|
348
|
+
return 'back';
|
|
349
|
+
draft.shouldInstall = confirmed;
|
|
350
|
+
return 'next';
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
];
|
|
354
|
+
const outcome = yield* Effect.promise(async () => {
|
|
355
|
+
let index = 0;
|
|
356
|
+
while (index < steps.length) {
|
|
357
|
+
const step = steps[index];
|
|
358
|
+
if (step === undefined || step.skip()) {
|
|
359
|
+
index += 1;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const result = await step.run();
|
|
363
|
+
if (result === 'cancel')
|
|
364
|
+
return 'cancelled';
|
|
365
|
+
if (result === 'back') {
|
|
366
|
+
// Walk back to the previous non-skipped step; Esc on the first
|
|
367
|
+
// step just re-asks it.
|
|
368
|
+
let previous = index - 1;
|
|
369
|
+
while (previous >= 0 && (steps[previous]?.skip() ?? true))
|
|
370
|
+
previous -= 1;
|
|
371
|
+
index = Math.max(0, previous);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
index += 1;
|
|
375
|
+
}
|
|
376
|
+
return 'done';
|
|
377
|
+
});
|
|
378
|
+
if (outcome === 'cancelled')
|
|
379
|
+
return yield* cancelled();
|
|
380
|
+
}
|
|
381
|
+
name = draft.name;
|
|
382
|
+
const templateId = draft.templateId;
|
|
383
|
+
const uiExtension = draft.uiExtension;
|
|
384
|
+
const distribution = draft.distribution;
|
|
385
|
+
const auth = draft.auth;
|
|
386
|
+
const appName = draft.appName?.trim() || toDisplayName(name);
|
|
387
|
+
const shouldInstall = interactive && !flagYes
|
|
388
|
+
? !flagSkipInstall && draft.shouldInstall
|
|
389
|
+
: flagYes && !flagSkipInstall;
|
|
390
|
+
const root = pathSvc.resolve(workingCwd, name);
|
|
391
|
+
const configPath = pathSvc.join(root, 'hsx.config.ts');
|
|
392
|
+
const configExists = yield* fs.exists(configPath).pipe(Effect.orElseSucceed(() => false));
|
|
393
|
+
if (configExists && !force) {
|
|
394
|
+
const code = 'HSX_E_INIT_DIR_NOT_EMPTY';
|
|
395
|
+
yield* reporter.error(code, `Project already exists at ${root}.`, {
|
|
396
|
+
hint: `Pick a new directory, delete the existing one (rm -rf ${root}), or pass --force to overwrite.`,
|
|
397
|
+
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
398
|
+
});
|
|
399
|
+
yield* reporter.done(undefined, 10);
|
|
400
|
+
return yield* Effect.fail(exitWith(10));
|
|
401
|
+
}
|
|
402
|
+
if (interactive)
|
|
403
|
+
yield* reporter.header(name);
|
|
404
|
+
else if (!json && !bannerShown) {
|
|
405
|
+
yield* reporter.banner;
|
|
406
|
+
yield* reporter.header(name);
|
|
407
|
+
}
|
|
408
|
+
const template = getTemplate(templateId);
|
|
409
|
+
const files = template.files(name, { appName, distribution, auth, uiExtension });
|
|
410
|
+
yield* fs
|
|
411
|
+
.makeDirectory(pathSvc.join(root, 'src', 'workers'), { recursive: true })
|
|
412
|
+
.pipe(Effect.mapError((cause) => cliError('HSX_E_INTERNAL', `Could not create ${root}: ${String(cause)}`)));
|
|
413
|
+
const scaffoldStep = yield* reporter.step('Scaffolding project');
|
|
414
|
+
for (const file of files) {
|
|
415
|
+
const filePath = pathSvc.join(root, file.path);
|
|
416
|
+
yield* fs
|
|
417
|
+
.makeDirectory(pathSvc.dirname(filePath), { recursive: true })
|
|
418
|
+
.pipe(Effect.mapError((cause) => cliError('HSX_E_INTERNAL', `Could not create directory for ${filePath}: ${String(cause)}`)));
|
|
419
|
+
yield* fs
|
|
420
|
+
.writeFileString(filePath, file.contents, { flag: force ? 'w' : 'wx' })
|
|
421
|
+
.pipe(Effect.mapError((cause) => cliError('HSX_E_INTERNAL', `Could not write ${filePath}: ${String(cause)}`)));
|
|
422
|
+
}
|
|
423
|
+
yield* scaffoldStep.ok(`${files.length} file${files.length === 1 ? '' : 's'} ${root}`);
|
|
424
|
+
if (shouldInstall) {
|
|
425
|
+
const installStep = yield* reporter.step('Installing dependencies');
|
|
426
|
+
const installed = yield* Effect.tryPromise({
|
|
427
|
+
try: () => runInstall(root),
|
|
428
|
+
catch: (error) => (error instanceof Error ? error.message : String(error)),
|
|
429
|
+
}).pipe(Effect.as(true), Effect.catchAll((message) => Effect.gen(function* () {
|
|
430
|
+
yield* installStep.fail(message);
|
|
431
|
+
yield* reporter.warn('HSX_W_INIT_INSTALL_FAILED', `Dependency install failed; you can retry with: cd ${name} && bun install`);
|
|
432
|
+
return false;
|
|
433
|
+
})));
|
|
434
|
+
if (installed)
|
|
435
|
+
yield* installStep.ok();
|
|
436
|
+
}
|
|
437
|
+
const result = {
|
|
438
|
+
ok: true,
|
|
439
|
+
command: 'init',
|
|
440
|
+
project: {
|
|
441
|
+
name,
|
|
442
|
+
appName,
|
|
443
|
+
root,
|
|
444
|
+
template: templateId,
|
|
445
|
+
distribution,
|
|
446
|
+
auth,
|
|
447
|
+
uiExtension,
|
|
448
|
+
installed: shouldInstall,
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
if (json) {
|
|
452
|
+
yield* reporter.raw(`${JSON.stringify(result, null, 2)}\n`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
yield* reporter.info('');
|
|
456
|
+
yield* reporter.info('Next steps:');
|
|
457
|
+
yield* reporter.info(` cd ${name}`);
|
|
458
|
+
if (!shouldInstall)
|
|
459
|
+
yield* reporter.info(' bun install');
|
|
460
|
+
yield* reporter.info(' hs-x dev');
|
|
461
|
+
yield* reporter.done('Created');
|
|
462
|
+
});
|
|
463
|
+
}
|
|
245
464
|
function makeInitCmd(commandName) {
|
|
246
465
|
return Command.make(commandName, {
|
|
247
466
|
name: nameArg,
|
|
@@ -250,54 +469,21 @@ function makeInitCmd(commandName) {
|
|
|
250
469
|
type: typeOption,
|
|
251
470
|
appName: appNameOption,
|
|
252
471
|
displayName: displayNameOption,
|
|
472
|
+
distribution: distributionOption,
|
|
473
|
+
auth: authOption,
|
|
474
|
+
uiExtension: uiExtensionOption,
|
|
475
|
+
noUiExtension: noUiExtensionOption,
|
|
253
476
|
yes: yesOption,
|
|
254
477
|
noInstall: noInstallOption,
|
|
255
478
|
force: forceOption,
|
|
256
479
|
json: jsonOption,
|
|
257
480
|
cwd: cwdOption,
|
|
258
|
-
}, (opts) =>
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// flag-like value as a missing name and restore the swallowed flag so
|
|
265
|
-
// it isn't lost (matches the legacy hardened parser).
|
|
266
|
-
let jsonOn = opts.json;
|
|
267
|
-
const argv = ['init'];
|
|
268
|
-
if (Option.isSome(opts.name))
|
|
269
|
-
argv.push(opts.name.value);
|
|
270
|
-
if (Option.isSome(opts.nameFlag)) {
|
|
271
|
-
const value = opts.nameFlag.value;
|
|
272
|
-
if (value.startsWith('-')) {
|
|
273
|
-
if (value === '--json')
|
|
274
|
-
jsonOn = true;
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
argv.push('--name', value);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
if (Option.isSome(opts.template))
|
|
281
|
-
argv.push('--template', opts.template.value);
|
|
282
|
-
if (Option.isSome(opts.type))
|
|
283
|
-
argv.push('--type', opts.type.value);
|
|
284
|
-
if (Option.isSome(opts.appName))
|
|
285
|
-
argv.push('--app-name', opts.appName.value);
|
|
286
|
-
if (Option.isSome(opts.displayName)) {
|
|
287
|
-
argv.push('--display-name', opts.displayName.value);
|
|
288
|
-
}
|
|
289
|
-
if (opts.yes)
|
|
290
|
-
argv.push('--yes');
|
|
291
|
-
if (opts.noInstall)
|
|
292
|
-
argv.push('--no-install');
|
|
293
|
-
if (opts.force)
|
|
294
|
-
argv.push('--force');
|
|
295
|
-
if (jsonOn)
|
|
296
|
-
argv.push('--json');
|
|
297
|
-
const cwdOverride = Option.getOrUndefined(opts.cwd);
|
|
298
|
-
const cwd = cwdOverride ?? (yield* Cwd);
|
|
299
|
-
yield* runQuarantined(jsonOn, () => initProject({ argv, cwd, json: jsonOn }));
|
|
300
|
-
})));
|
|
481
|
+
}, (opts) => {
|
|
482
|
+
// Restore the swallowed `--json` for the reporter flag too (see initBody).
|
|
483
|
+
const json = opts.json ||
|
|
484
|
+
(Option.isSome(opts.nameFlag) && opts.nameFlag.value === '--json');
|
|
485
|
+
return runHandler(commandName, { json }, initBody(opts));
|
|
486
|
+
});
|
|
301
487
|
}
|
|
302
488
|
export const initCmd = makeInitCmd('init');
|
|
303
489
|
export const createCmd = makeInitCmd('create');
|
|
@@ -311,6 +497,9 @@ Options:
|
|
|
311
497
|
--type <type> App type: workflow-action | sync-source | empty
|
|
312
498
|
--template <id> Starter template id: deals-tagger | empty | sync-source
|
|
313
499
|
--app-name <name> HubSpot app display name (defaults from project name)
|
|
500
|
+
--distribution <mode> private (default) | marketplace
|
|
501
|
+
--auth <mode> oauth (default) | static
|
|
502
|
+
--ui-extension Scaffold a starter UI-extension app card (--no-ui-extension to skip)
|
|
314
503
|
--yes, -y Accept defaults (install deps, no prompts)
|
|
315
504
|
--no-install Skip dependency install
|
|
316
505
|
--force Overwrite an existing hsx.config.ts at target
|