@nocobase/cli 2.3.0-alpha.1 → 3.0.0-alpha.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.
Files changed (43) hide show
  1. package/assets/env-proxy/nginx/snippets/uploads-location.conf +4 -1
  2. package/dist/commands/config/set.js +1 -0
  3. package/dist/commands/init.js +13 -5
  4. package/dist/commands/install.js +104 -3
  5. package/dist/commands/portal/config.js +88 -0
  6. package/dist/commands/portal/create.js +105 -0
  7. package/dist/commands/portal/deploy.js +81 -0
  8. package/dist/commands/portal/destroy.js +104 -0
  9. package/dist/commands/portal/dev.js +71 -0
  10. package/dist/commands/portal/index.js +20 -0
  11. package/dist/commands/portal/info.js +82 -0
  12. package/dist/commands/portal/list.js +98 -0
  13. package/dist/commands/portal/pull.js +84 -0
  14. package/dist/commands/portal/push.js +79 -0
  15. package/dist/commands/source/dev.js +1 -1
  16. package/dist/lib/api-client.js +7 -0
  17. package/dist/lib/auth-store.js +3 -1
  18. package/dist/lib/cli-config.js +20 -1
  19. package/dist/lib/env-auth.js +2 -2
  20. package/dist/lib/env-config.js +3 -0
  21. package/dist/lib/env-proxy.js +141 -8
  22. package/dist/lib/managed-env-file.js +58 -2
  23. package/dist/lib/managed-init-env.js +6 -1
  24. package/dist/lib/naming.js +9 -0
  25. package/dist/lib/portal-command-env.js +31 -0
  26. package/dist/lib/portal-config.js +133 -0
  27. package/dist/lib/portal-configure.js +117 -0
  28. package/dist/lib/portal-create.js +433 -0
  29. package/dist/lib/portal-deploy.js +283 -0
  30. package/dist/lib/portal-destroy.js +100 -0
  31. package/dist/lib/portal-dev.js +79 -0
  32. package/dist/lib/portal-env-files.js +53 -0
  33. package/dist/lib/portal-info.js +31 -0
  34. package/dist/lib/portal-list.js +211 -0
  35. package/dist/lib/portal-source.js +523 -0
  36. package/dist/lib/proxy-caddy.js +2 -0
  37. package/dist/lib/proxy-nginx.js +1 -0
  38. package/dist/lib/run-npm.js +17 -16
  39. package/dist/lib/ui.js +28 -1
  40. package/dist/locale/en-US.json +178 -0
  41. package/dist/locale/zh-CN.json +178 -0
  42. package/nocobase-ctl.config.json +111 -0
  43. package/package.json +5 -2
@@ -0,0 +1,523 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { execFile } from 'node:child_process';
10
+ import { promisify } from 'node:util';
11
+ import { cp, mkdir, mkdtemp, readdir, rename, rm, stat } from 'node:fs/promises';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ import * as tar from 'tar';
15
+ import { executeApiRequest } from './api-client.js';
16
+ import { translateCli } from './cli-locale.js';
17
+ import { buildPortalConfig, buildPortalConfigFromOptions, DEFAULT_PORTAL_GIT_PATH, readPortalConfig, syncPortalConfigToRemote, writePortalConfig, } from './portal-config.js';
18
+ import { buildPortalCommandEnv } from './portal-command-env.js';
19
+ import { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
20
+ import { listPortalWorkspaces } from './portal-list.js';
21
+ import { findPortalListItem } from './portal-info.js';
22
+ import { run } from './run-npm.js';
23
+ const execFileAsync = promisify(execFile);
24
+ const portalSourceText = (key, values, fallback) => translateCli(`commands.portalSource.${key}`, values, { fallback });
25
+ const PULL_SOURCE_OPERATION = {
26
+ method: 'POST',
27
+ pathTemplate: '/multiPortals:pullSource',
28
+ hasBody: true,
29
+ bodyRequired: true,
30
+ responseType: 'binary',
31
+ parameters: [
32
+ {
33
+ name: 'app',
34
+ flagName: 'app',
35
+ in: 'body',
36
+ required: true,
37
+ },
38
+ {
39
+ name: 'portal',
40
+ flagName: 'portal',
41
+ in: 'body',
42
+ required: true,
43
+ },
44
+ ],
45
+ };
46
+ const PUSH_SOURCE_OPERATION = {
47
+ method: 'POST',
48
+ pathTemplate: '/multiPortals:pushSource',
49
+ requestContentType: 'multipart/form-data',
50
+ hasBody: true,
51
+ bodyRequired: true,
52
+ parameters: [
53
+ {
54
+ name: 'file',
55
+ flagName: 'file',
56
+ in: 'body',
57
+ required: true,
58
+ isFile: true,
59
+ },
60
+ {
61
+ name: 'app',
62
+ flagName: 'app',
63
+ in: 'body',
64
+ required: true,
65
+ },
66
+ {
67
+ name: 'portal',
68
+ flagName: 'portal',
69
+ in: 'body',
70
+ required: true,
71
+ },
72
+ {
73
+ name: 'message',
74
+ flagName: 'message',
75
+ in: 'body',
76
+ },
77
+ ],
78
+ };
79
+ function trimValue(value) {
80
+ return String(value ?? '').trim();
81
+ }
82
+ async function pathExists(target) {
83
+ try {
84
+ await stat(target);
85
+ return true;
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ async function isFile(target) {
92
+ try {
93
+ return (await stat(target)).isFile();
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ function shouldPackPortalSourceEntry(entryName) {
100
+ return !entryName
101
+ .split('/')
102
+ .some((segment) => segment.startsWith('._') || ['.git', 'node_modules', 'dist', '.DS_Store'].includes(segment));
103
+ }
104
+ function validatePortalSourceTarEntry(entryPath, entry) {
105
+ if (path.isAbsolute(entryPath) || entryPath.split(/[\\/]+/).includes('..')) {
106
+ return false;
107
+ }
108
+ const tarEntry = entry;
109
+ if (tarEntry.type === 'SymbolicLink' || tarEntry.type === 'Link' || typeof tarEntry.linkpath === 'string') {
110
+ return false;
111
+ }
112
+ return shouldPackPortalSourceEntry(entryPath);
113
+ }
114
+ async function packPortalSource(portalDir) {
115
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-source-'));
116
+ const archivePath = path.join(tempDir, 'source.tar.gz');
117
+ const entries = (await readdir(portalDir)).filter(shouldPackPortalSourceEntry);
118
+ await tar.create({
119
+ cwd: portalDir,
120
+ file: archivePath,
121
+ gzip: true,
122
+ filter: (entryPath, entry) => validatePortalSourceTarEntry(entryPath, entry),
123
+ }, entries);
124
+ return {
125
+ archivePath,
126
+ cleanup: async () => {
127
+ await rm(tempDir, { recursive: true, force: true });
128
+ },
129
+ };
130
+ }
131
+ async function replacePortalSourceFromArchive(params) {
132
+ const targetExists = await pathExists(params.portalDir);
133
+ if (targetExists && !params.force) {
134
+ throw new Error(portalSourceText('errors.workspaceExists', { portalDir: params.portalDir }, `Portal already exists: ${params.portalDir}\nPass --force to delete it and pull again.`));
135
+ }
136
+ const parentDir = path.dirname(params.portalDir);
137
+ const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
138
+ try {
139
+ await tar.extract({
140
+ file: params.archivePath,
141
+ cwd: tempDir,
142
+ strict: true,
143
+ filter: validatePortalSourceTarEntry,
144
+ });
145
+ if (targetExists) {
146
+ await rm(params.portalDir, { recursive: true, force: true });
147
+ }
148
+ await rename(tempDir, params.portalDir);
149
+ }
150
+ catch (error) {
151
+ await rm(tempDir, { recursive: true, force: true });
152
+ throw error;
153
+ }
154
+ }
155
+ async function replacePortalSourceFromDirectory(params) {
156
+ const targetExists = await pathExists(params.portalDir);
157
+ if (targetExists && !params.force) {
158
+ throw new Error(portalSourceText('errors.workspaceExists', { portalDir: params.portalDir }, `Portal already exists: ${params.portalDir}\nPass --force to delete it and pull again.`));
159
+ }
160
+ const parentDir = path.dirname(params.portalDir);
161
+ const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
162
+ try {
163
+ await cp(params.sourceDir, tempDir, {
164
+ recursive: true,
165
+ filter: (source) => shouldPackPortalSourceEntry(path.relative(params.sourceDir, source)),
166
+ });
167
+ if (targetExists) {
168
+ await rm(params.portalDir, { recursive: true, force: true });
169
+ }
170
+ await rename(tempDir, params.portalDir);
171
+ }
172
+ catch (error) {
173
+ await rm(tempDir, { recursive: true, force: true });
174
+ throw error;
175
+ }
176
+ }
177
+ async function runGit(args, cwd) {
178
+ return await execFileAsync('git', args, {
179
+ cwd,
180
+ maxBuffer: 10 * 1024 * 1024,
181
+ });
182
+ }
183
+ async function installPortalDependencies(params) {
184
+ if (params.installDependencies === false) {
185
+ return {
186
+ dependenciesInstalled: false,
187
+ installSkipped: true,
188
+ };
189
+ }
190
+ if (!(await isFile(path.join(params.portalDir, 'package.json')))) {
191
+ return {
192
+ dependenciesInstalled: false,
193
+ installSkipped: true,
194
+ };
195
+ }
196
+ const runCommand = params.runCommand ?? run;
197
+ await runCommand('pnpm', ['install'], {
198
+ cwd: params.portalDir,
199
+ env: buildPortalCommandEnv(),
200
+ envMode: 'replace',
201
+ errorName: 'pnpm install',
202
+ });
203
+ return {
204
+ dependenciesInstalled: true,
205
+ installSkipped: false,
206
+ };
207
+ }
208
+ function readSourceRevision(data) {
209
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
210
+ return undefined;
211
+ }
212
+ const direct = data.sourceRevision;
213
+ if (typeof direct === 'string' && direct.trim()) {
214
+ return direct;
215
+ }
216
+ return readSourceRevision(data.data);
217
+ }
218
+ async function resolvePortalSourceContext(options) {
219
+ const portal = validatePortalSlug(options.portal);
220
+ const apiBaseUrl = trimValue(options.env.apiBaseUrl);
221
+ const storagePath = resolvePortalStoragePath(options.env);
222
+ const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
223
+ const portalDir = path.join(storagePath, 'portals', app, portal);
224
+ const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
225
+ const mode = options.env.kind;
226
+ if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
227
+ throw new Error(portalSourceText('errors.unsupportedEnvKind', { kind: mode }, `Cannot sync portal source for ${mode} envs in the first version.`));
228
+ }
229
+ const list = await listPortalWorkspaces({
230
+ env: options.env,
231
+ envName: options.envName,
232
+ cliVersion: options.cliVersion,
233
+ apiRequest: options.apiRequest,
234
+ });
235
+ const item = findPortalListItem(list.items, portal);
236
+ if (!item) {
237
+ throw new Error(portalSourceText('errors.notFound', { portal }, `Portal "${portal}" was not found. Run \`nb portal list\` to see available portals.`));
238
+ }
239
+ return {
240
+ app,
241
+ portal,
242
+ portalDir,
243
+ portalBase,
244
+ mode,
245
+ sourceStorage: item.sourceStorage || 'nocobase',
246
+ gitRepo: item.gitRepo,
247
+ gitBranch: item.gitBranch || 'main',
248
+ gitPath: item.gitPath || DEFAULT_PORTAL_GIT_PATH,
249
+ options: item.options,
250
+ };
251
+ }
252
+ function buildPortalConfigFromContext(context) {
253
+ if (Object.keys(context.options).length > 0) {
254
+ return buildPortalConfigFromOptions(context.options, context.portal);
255
+ }
256
+ const sourceStorage = context.sourceStorage || 'nocobase';
257
+ return buildPortalConfig({
258
+ portal: context.portal,
259
+ sourceStorage,
260
+ gitRepo: sourceStorage === 'git' ? context.gitRepo : undefined,
261
+ gitBranch: sourceStorage === 'git' ? context.gitBranch : undefined,
262
+ gitPath: sourceStorage === 'git' ? context.gitPath : undefined,
263
+ });
264
+ }
265
+ function applyPortalConfigToContext(context, config) {
266
+ return {
267
+ ...context,
268
+ sourceStorage: config.sourceStorage,
269
+ gitRepo: config.git?.repo ?? '',
270
+ gitBranch: config.git?.branch ?? 'main',
271
+ gitPath: config.git?.path ?? DEFAULT_PORTAL_GIT_PATH,
272
+ };
273
+ }
274
+ function assertGitSourceConfig(context) {
275
+ if (!context.gitRepo) {
276
+ throw new Error(portalSourceText('errors.gitRepoMissing', { portal: context.portal }, `Portal "${context.portal}" uses Git source storage, but gitRepo is missing.`));
277
+ }
278
+ return {
279
+ repo: context.gitRepo,
280
+ branch: context.gitBranch || 'main',
281
+ gitPath: context.gitPath || DEFAULT_PORTAL_GIT_PATH,
282
+ };
283
+ }
284
+ function isGitRepositoryRootPath(gitPath) {
285
+ return gitPath === DEFAULT_PORTAL_GIT_PATH;
286
+ }
287
+ async function copyPortalSourceToGitPath(params) {
288
+ if (isGitRepositoryRootPath(params.gitPath)) {
289
+ const existingEntries = await readdir(params.repoDir);
290
+ await Promise.all(existingEntries
291
+ .filter((entry) => entry !== '.git')
292
+ .map((entry) => rm(path.join(params.repoDir, entry), { recursive: true, force: true })));
293
+ const sourceEntries = (await readdir(params.portalDir)).filter(shouldPackPortalSourceEntry);
294
+ await Promise.all(sourceEntries.map((entry) => cp(path.join(params.portalDir, entry), path.join(params.repoDir, entry), {
295
+ recursive: true,
296
+ filter: (source) => shouldPackPortalSourceEntry(path.relative(params.portalDir, source)),
297
+ })));
298
+ return;
299
+ }
300
+ const targetDir = path.join(params.repoDir, params.gitPath);
301
+ await rm(targetDir, { recursive: true, force: true });
302
+ await mkdir(path.dirname(targetDir), { recursive: true });
303
+ await cp(params.portalDir, targetDir, {
304
+ recursive: true,
305
+ filter: (source) => shouldPackPortalSourceEntry(path.relative(params.portalDir, source)),
306
+ });
307
+ }
308
+ async function cloneGitSource(params) {
309
+ const repoDir = path.join(params.cwd, 'repo');
310
+ try {
311
+ await runGit(['clone', '--branch', params.branch, params.repo, repoDir]);
312
+ }
313
+ catch (error) {
314
+ if (!params.createBranch) {
315
+ throw error;
316
+ }
317
+ await runGit(['clone', params.repo, repoDir]);
318
+ await runGit(['checkout', '-B', params.branch], repoDir);
319
+ }
320
+ return repoDir;
321
+ }
322
+ async function pullGitPortalSource(params) {
323
+ const git = assertGitSourceConfig(params.context);
324
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-git-pull-'));
325
+ try {
326
+ const repoDir = await cloneGitSource({
327
+ repo: git.repo,
328
+ branch: git.branch,
329
+ cwd: tempDir,
330
+ createBranch: true,
331
+ });
332
+ const sourceDir = path.join(repoDir, git.gitPath);
333
+ if (!(await pathExists(sourceDir))) {
334
+ throw new Error(portalSourceText('errors.gitPathMissing', { gitPath: git.gitPath }, `Git path does not exist in the configured repository: ${git.gitPath}`));
335
+ }
336
+ await mkdir(path.dirname(params.context.portalDir), { recursive: true });
337
+ await replacePortalSourceFromDirectory({
338
+ sourceDir,
339
+ portalDir: params.context.portalDir,
340
+ force: params.force,
341
+ });
342
+ }
343
+ finally {
344
+ await rm(tempDir, { recursive: true, force: true });
345
+ }
346
+ }
347
+ async function pushGitPortalSource(params) {
348
+ const git = assertGitSourceConfig(params.context);
349
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-git-push-'));
350
+ try {
351
+ const repoDir = await cloneGitSource({
352
+ repo: git.repo,
353
+ branch: git.branch,
354
+ cwd: tempDir,
355
+ createBranch: true,
356
+ });
357
+ await copyPortalSourceToGitPath({
358
+ portalDir: params.context.portalDir,
359
+ repoDir,
360
+ gitPath: git.gitPath,
361
+ });
362
+ await runGit(['add', git.gitPath], repoDir);
363
+ const status = await runGit(['status', '--porcelain', '--', git.gitPath], repoDir);
364
+ if (!status.stdout.trim()) {
365
+ return undefined;
366
+ }
367
+ await runGit([
368
+ '-c',
369
+ 'user.name=NocoBase CLI',
370
+ '-c',
371
+ 'user.email=nocobase-cli@localhost',
372
+ 'commit',
373
+ '-m',
374
+ trimValue(params.message) || `chore(portal): update ${params.context.portal}`,
375
+ ], repoDir);
376
+ await runGit(['push', 'origin', git.branch], repoDir);
377
+ const revision = await runGit(['rev-parse', 'HEAD'], repoDir);
378
+ return revision.stdout.trim();
379
+ }
380
+ finally {
381
+ await rm(tempDir, { recursive: true, force: true });
382
+ }
383
+ }
384
+ export async function pullPortalSource(options) {
385
+ const context = await resolvePortalSourceContext(options);
386
+ const portalConfig = buildPortalConfigFromContext(context);
387
+ const sourceContext = applyPortalConfigToContext(context, portalConfig);
388
+ if (sourceContext.sourceStorage === 'git') {
389
+ await pullGitPortalSource({
390
+ context: sourceContext,
391
+ force: options.force,
392
+ });
393
+ await writePortalConfig(sourceContext.portalDir, portalConfig);
394
+ const installResult = await installPortalDependencies({
395
+ portalDir: sourceContext.portalDir,
396
+ installDependencies: options.installDependencies,
397
+ runCommand: options.runCommand,
398
+ });
399
+ return {
400
+ ...sourceContext,
401
+ changed: true,
402
+ ...installResult,
403
+ };
404
+ }
405
+ if (sourceContext.sourceStorage !== 'nocobase') {
406
+ throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
407
+ }
408
+ if (sourceContext.mode === 'local' || sourceContext.mode === 'docker') {
409
+ return {
410
+ ...sourceContext,
411
+ changed: false,
412
+ noopReason: sourceContext.mode === 'local'
413
+ ? portalSourceText('messages.localPullNoop', undefined, 'Portal source is already local.')
414
+ : portalSourceText('messages.dockerPullNoop', undefined, 'Portal source is already available through the Docker volume.'),
415
+ };
416
+ }
417
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-pull-'));
418
+ const archivePath = path.join(tempDir, 'source.tar.gz');
419
+ const apiRequest = options.apiRequest ?? executeApiRequest;
420
+ try {
421
+ const response = await apiRequest({
422
+ cliVersion: options.cliVersion ?? '',
423
+ envName: options.envName,
424
+ flags: {
425
+ app: sourceContext.app,
426
+ portal: sourceContext.portal,
427
+ output: archivePath,
428
+ },
429
+ operation: PULL_SOURCE_OPERATION,
430
+ });
431
+ if (!response.ok) {
432
+ throw new Error(portalSourceText('errors.pullFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal source pull failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
433
+ }
434
+ await mkdir(path.dirname(sourceContext.portalDir), { recursive: true });
435
+ await replacePortalSourceFromArchive({
436
+ archivePath,
437
+ portalDir: sourceContext.portalDir,
438
+ force: options.force,
439
+ });
440
+ await writePortalConfig(sourceContext.portalDir, portalConfig);
441
+ const installResult = await installPortalDependencies({
442
+ portalDir: sourceContext.portalDir,
443
+ installDependencies: options.installDependencies,
444
+ runCommand: options.runCommand,
445
+ });
446
+ return {
447
+ ...sourceContext,
448
+ changed: true,
449
+ ...installResult,
450
+ };
451
+ }
452
+ finally {
453
+ await rm(tempDir, { recursive: true, force: true });
454
+ }
455
+ }
456
+ export async function pushPortalSource(options) {
457
+ const context = await resolvePortalSourceContext(options);
458
+ if (!(await pathExists(context.portalDir))) {
459
+ throw new Error(portalSourceText('errors.workspaceMissing', { portalDir: context.portalDir, portal: context.portal }, `Portal does not exist: ${context.portalDir}\nRun \`nb portal create ${context.portal}\` first.`));
460
+ }
461
+ const portalConfig = await readPortalConfig(context.portalDir);
462
+ await syncPortalConfigToRemote({
463
+ portal: context.portal,
464
+ config: portalConfig,
465
+ currentOptions: context.options,
466
+ envName: options.envName,
467
+ cliVersion: options.cliVersion,
468
+ apiRequest: options.apiRequest,
469
+ });
470
+ const sourceContext = applyPortalConfigToContext(context, portalConfig);
471
+ if (sourceContext.sourceStorage === 'git') {
472
+ const revision = await pushGitPortalSource({
473
+ context: sourceContext,
474
+ message: options.message,
475
+ });
476
+ return {
477
+ ...sourceContext,
478
+ changed: Boolean(revision),
479
+ sourceRevision: revision,
480
+ noopReason: revision
481
+ ? undefined
482
+ : portalSourceText('messages.gitPushNoop', undefined, 'No local source changes to push.'),
483
+ };
484
+ }
485
+ if (sourceContext.sourceStorage !== 'nocobase') {
486
+ throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
487
+ }
488
+ if (sourceContext.mode === 'local' || sourceContext.mode === 'docker') {
489
+ return {
490
+ ...sourceContext,
491
+ changed: false,
492
+ noopReason: sourceContext.mode === 'local'
493
+ ? portalSourceText('messages.localPushNoop', undefined, 'Portal source is already local.')
494
+ : portalSourceText('messages.dockerPushNoop', undefined, 'Portal source is already available through the Docker volume.'),
495
+ };
496
+ }
497
+ const archive = await packPortalSource(sourceContext.portalDir);
498
+ const apiRequest = options.apiRequest ?? executeApiRequest;
499
+ try {
500
+ const response = await apiRequest({
501
+ cliVersion: options.cliVersion ?? '',
502
+ envName: options.envName,
503
+ flags: {
504
+ file: archive.archivePath,
505
+ app: sourceContext.app,
506
+ portal: sourceContext.portal,
507
+ message: options.message,
508
+ },
509
+ operation: PUSH_SOURCE_OPERATION,
510
+ });
511
+ if (!response.ok) {
512
+ throw new Error(portalSourceText('errors.pushFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal source push failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
513
+ }
514
+ return {
515
+ ...sourceContext,
516
+ changed: true,
517
+ sourceRevision: readSourceRevision(response.data),
518
+ };
519
+ }
520
+ finally {
521
+ await archive.cleanup();
522
+ }
523
+ }
@@ -79,6 +79,7 @@ export async function writeCaddyProxyBundle(runtime, appEntryOptions, runtimeCon
79
79
  writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
80
80
  writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
81
81
  writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
82
+ writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
82
83
  writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
83
84
  ]);
84
85
  return {
@@ -100,6 +101,7 @@ export async function writeManualCaddyProxyBundle(input, appEntryOptions, runtim
100
101
  writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
101
102
  writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
102
103
  writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
104
+ writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
103
105
  writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
104
106
  ]);
105
107
  return {
@@ -107,6 +107,7 @@ async function writeResolvedNginxProxyBundle(bundle, appEntryOptions, options) {
107
107
  writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
108
108
  writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
109
109
  writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
110
+ writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
110
111
  writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
111
112
  syncEnvProxyNginxSnippets(),
112
113
  ]);
@@ -48,6 +48,10 @@ const MISSING_COMMAND_SPECS = {
48
48
  displayName: 'pnpm',
49
49
  configKey: 'bin.pnpm',
50
50
  },
51
+ npm: {
52
+ displayName: 'npm',
53
+ configKey: 'bin.npm',
54
+ },
51
55
  };
52
56
  const DOCKER_DAEMON_UNAVAILABLE_PATTERNS = [
53
57
  /cannot connect to the docker daemon/i,
@@ -61,6 +65,15 @@ async function resolveCommandName(name) {
61
65
  function shouldTeeInheritedOutput(options) {
62
66
  return options?.stdio === 'inherit' && Boolean(String(process.env.NB_CLI_ACTIVE_LOG_FILE ?? '').trim());
63
67
  }
68
+ function buildProcessEnv(options) {
69
+ if (options?.envMode === 'replace') {
70
+ return options.env ?? {};
71
+ }
72
+ return {
73
+ ...process.env,
74
+ ...options?.env,
75
+ };
76
+ }
64
77
  function createMissingCommandError(name, label, error) {
65
78
  const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : undefined;
66
79
  if (code !== 'ENOENT') {
@@ -146,10 +159,7 @@ export async function run(name, args, options) {
146
159
  const child = spawn(command, [...args], {
147
160
  stdio,
148
161
  cwd,
149
- env: {
150
- ...process.env,
151
- ...options?.env,
152
- },
162
+ env: buildProcessEnv(options),
153
163
  windowsHide: process.platform === 'win32',
154
164
  });
155
165
  if (options?.stdio === 'pipe' || shouldTeeInheritedOutput(options)) {
@@ -268,10 +278,7 @@ export async function commandSucceeds(name, args, options) {
268
278
  return await new Promise((resolve, reject) => {
269
279
  const child = spawn(command, [...args], {
270
280
  cwd,
271
- env: {
272
- ...process.env,
273
- ...options?.env,
274
- },
281
+ env: buildProcessEnv(options),
275
282
  stdio: 'ignore',
276
283
  windowsHide: process.platform === 'win32',
277
284
  });
@@ -302,10 +309,7 @@ export async function commandOutput(name, args, options) {
302
309
  return await new Promise((resolve, reject) => {
303
310
  const child = spawn(command, [...args], {
304
311
  cwd,
305
- env: {
306
- ...process.env,
307
- ...options?.env,
308
- },
312
+ env: buildProcessEnv(options),
309
313
  stdio: ['ignore', 'pipe', 'pipe'],
310
314
  windowsHide: process.platform === 'win32',
311
315
  });
@@ -364,10 +368,7 @@ export async function commandOutputViaFile(name, args, options) {
364
368
  const result = await new Promise((resolve, reject) => {
365
369
  const child = spawn(command, [...args], {
366
370
  cwd,
367
- env: {
368
- ...process.env,
369
- ...options?.env,
370
- },
371
+ env: buildProcessEnv(options),
371
372
  stdio: ['ignore', stdoutHandle.fd, stderrHandle.fd],
372
373
  windowsHide: process.platform === 'win32',
373
374
  });
package/dist/lib/ui.js CHANGED
@@ -15,8 +15,35 @@ let verboseMode = false;
15
15
  let lastStaticTaskMessage;
16
16
  let lastStaticTaskAt = 0;
17
17
  const STATIC_TASK_UPDATE_THROTTLE_MS = 3_000;
18
+ function isCombiningCodePoint(codePoint) {
19
+ return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
20
+ (codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
21
+ (codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
22
+ (codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
23
+ (codePoint >= 0xfe20 && codePoint <= 0xfe2f));
24
+ }
25
+ function isFullWidthCodePoint(codePoint) {
26
+ return (codePoint >= 0x1100 &&
27
+ (codePoint <= 0x115f ||
28
+ codePoint === 0x2329 ||
29
+ codePoint === 0x232a ||
30
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
31
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
32
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
33
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
34
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
35
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
36
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
37
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)));
38
+ }
18
39
  function stringWidth(value) {
19
- return Array.from(value).length;
40
+ return Array.from(value).reduce((width, character) => {
41
+ const codePoint = character.codePointAt(0);
42
+ if (!codePoint || codePoint === 0 || codePoint < 32 || isCombiningCodePoint(codePoint)) {
43
+ return width;
44
+ }
45
+ return width + (isFullWidthCodePoint(codePoint) ? 2 : 1);
46
+ }, 0);
20
47
  }
21
48
  function pad(value, width) {
22
49
  const padding = Math.max(0, width - stringWidth(value));