@junoflow/cli 0.6.0 → 0.10.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/README.md +214 -80
- package/app-client.js +306 -0
- package/eslint.app.config.js +34 -0
- package/eslint.flow.config.js +4 -2
- package/package.json +9 -4
- package/src/api.js +213 -51
- package/src/app-dev/build.js +134 -0
- package/src/app-dev/harness.js +108 -0
- package/src/app-dev/panel.html +336 -0
- package/src/app-dev/panel.js +290 -0
- package/src/app-dev/server.js +347 -0
- package/src/app-dev/stream.js +224 -0
- package/src/app-dev/watch.js +47 -0
- package/src/app-project.js +133 -0
- package/src/authoring-skills.js +2 -1
- package/src/commands/app-check.js +75 -0
- package/src/commands/app-deploy.js +113 -0
- package/src/commands/app-dev.js +141 -0
- package/src/commands/app-gen-types.js +37 -0
- package/src/commands/app-init.js +46 -0
- package/src/commands/app-pull.js +106 -0
- package/src/commands/check.js +33 -24
- package/src/commands/config.js +26 -7
- package/src/commands/deploy.js +59 -13
- package/src/commands/flow-check.js +4 -22
- package/src/commands/flow-deploy.js +11 -8
- package/src/commands/flow-fixture.js +67 -0
- package/src/commands/flow-gen-types.js +12 -5
- package/src/commands/flow-pull.js +69 -40
- package/src/commands/gen-types.js +2 -9
- package/src/commands/init.js +2 -9
- package/src/commands/pull.js +98 -111
- package/src/config.js +20 -5
- package/src/errors.js +3 -3
- package/src/exit.js +2 -2
- package/src/flow-project.js +47 -97
- package/src/index.js +99 -23
- package/src/project.js +76 -8
- package/src/token.js +30 -0
- package/src/watch.js +48 -11
- package/src/working-copy.js +262 -0
- package/tsconfig.page.json +15 -0
- package/verbs.json +52 -3
package/src/index.js
CHANGED
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
AGENT_SKILL_INSTALL_USAGE,
|
|
5
5
|
agentSkillInstall,
|
|
6
6
|
} from './commands/agent-skill-install.js';
|
|
7
|
+
import { APP_CHECK_USAGE, appCheck } from './commands/app-check.js';
|
|
8
|
+
import { APP_DEPLOY_USAGE, appDeploy } from './commands/app-deploy.js';
|
|
9
|
+
import { APP_DEV_USAGE, appDev } from './commands/app-dev.js';
|
|
10
|
+
import { APP_GEN_TYPES_USAGE, appGenTypes } from './commands/app-gen-types.js';
|
|
11
|
+
import { APP_INIT_USAGE, appInit } from './commands/app-init.js';
|
|
12
|
+
import { APP_PULL_USAGE, appPull } from './commands/app-pull.js';
|
|
7
13
|
import { ASK_USAGE, ask } from './commands/ask.js';
|
|
8
14
|
import { CHECK_USAGE, check } from './commands/check.js';
|
|
9
15
|
import { CONFIG_SET_USAGE, configSet } from './commands/config.js';
|
|
@@ -18,6 +24,7 @@ import {
|
|
|
18
24
|
FLOW_GEN_TYPES_USAGE,
|
|
19
25
|
flowGenTypes,
|
|
20
26
|
} from './commands/flow-gen-types.js';
|
|
27
|
+
import { FLOW_FIXTURE_USAGE, flowFixture } from './commands/flow-fixture.js';
|
|
21
28
|
import { FLOW_INIT_USAGE, flowInit } from './commands/flow-init.js';
|
|
22
29
|
import { FLOW_PULL_USAGE, flowPull } from './commands/flow-pull.js';
|
|
23
30
|
import { GEN_TYPES_USAGE, genTypes } from './commands/gen-types.js';
|
|
@@ -55,7 +62,11 @@ import {
|
|
|
55
62
|
import { closest, closestByVerb, levenshtein } from './suggest.js';
|
|
56
63
|
import { groupOf, normalizeVerb, rowFor, rows } from './verbs.js';
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
/**
|
|
66
|
+
* The two flags every command that reaches an instance takes, and the two a
|
|
67
|
+
* leading position accepts (`juno --url <u> --token <k> state get x`).
|
|
68
|
+
*/
|
|
69
|
+
const URL_OPTION = { url: { type: 'string' }, token: { type: 'string' } };
|
|
59
70
|
|
|
60
71
|
/**
|
|
61
72
|
* The verbs.json rows that call no tool, by command. A row is dispatchable
|
|
@@ -151,6 +162,15 @@ const WORKING_COPY = {
|
|
|
151
162
|
options: { ...URL_OPTION, force: { type: 'boolean', default: false } },
|
|
152
163
|
usage: FLOW_PULL_USAGE,
|
|
153
164
|
},
|
|
165
|
+
'flow fixture': {
|
|
166
|
+
run: flowFixture,
|
|
167
|
+
options: {
|
|
168
|
+
...URL_OPTION,
|
|
169
|
+
run: { type: 'string' },
|
|
170
|
+
force: { type: 'boolean', default: false },
|
|
171
|
+
},
|
|
172
|
+
usage: FLOW_FIXTURE_USAGE,
|
|
173
|
+
},
|
|
154
174
|
'integration init': {
|
|
155
175
|
run: init,
|
|
156
176
|
options: {
|
|
@@ -189,6 +209,7 @@ const WORKING_COPY = {
|
|
|
189
209
|
...URL_OPTION,
|
|
190
210
|
id: { type: 'string' },
|
|
191
211
|
watch: { type: 'boolean', default: false },
|
|
212
|
+
force: { type: 'boolean', default: false },
|
|
192
213
|
json: { type: 'boolean', default: false },
|
|
193
214
|
},
|
|
194
215
|
usage: DEPLOY_USAGE,
|
|
@@ -202,6 +223,50 @@ const WORKING_COPY = {
|
|
|
202
223
|
},
|
|
203
224
|
usage: PULL_USAGE,
|
|
204
225
|
},
|
|
226
|
+
'app init': {
|
|
227
|
+
run: appInit,
|
|
228
|
+
options: { ...URL_OPTION, here: { type: 'boolean', default: false } },
|
|
229
|
+
usage: APP_INIT_USAGE,
|
|
230
|
+
positional: true,
|
|
231
|
+
},
|
|
232
|
+
'app gen-types': {
|
|
233
|
+
run: appGenTypes,
|
|
234
|
+
options: URL_OPTION,
|
|
235
|
+
usage: APP_GEN_TYPES_USAGE,
|
|
236
|
+
},
|
|
237
|
+
'app check': {
|
|
238
|
+
run: appCheck,
|
|
239
|
+
options: {
|
|
240
|
+
...URL_OPTION,
|
|
241
|
+
'skip-local': { type: 'boolean', default: false },
|
|
242
|
+
json: { type: 'boolean', default: false },
|
|
243
|
+
},
|
|
244
|
+
usage: APP_CHECK_USAGE,
|
|
245
|
+
},
|
|
246
|
+
'app deploy': {
|
|
247
|
+
run: appDeploy,
|
|
248
|
+
options: {
|
|
249
|
+
...URL_OPTION,
|
|
250
|
+
watch: { type: 'boolean', default: false },
|
|
251
|
+
force: { type: 'boolean', default: false },
|
|
252
|
+
json: { type: 'boolean', default: false },
|
|
253
|
+
},
|
|
254
|
+
usage: APP_DEPLOY_USAGE,
|
|
255
|
+
},
|
|
256
|
+
'app pull': {
|
|
257
|
+
run: appPull,
|
|
258
|
+
options: { ...URL_OPTION, force: { type: 'boolean', default: false } },
|
|
259
|
+
usage: APP_PULL_USAGE,
|
|
260
|
+
},
|
|
261
|
+
// No --url: the dev server is offline and never reads juno.target.
|
|
262
|
+
'app dev': {
|
|
263
|
+
run: appDev,
|
|
264
|
+
options: {
|
|
265
|
+
port: { type: 'string' },
|
|
266
|
+
scenario: { type: 'string' },
|
|
267
|
+
},
|
|
268
|
+
usage: APP_DEV_USAGE,
|
|
269
|
+
},
|
|
205
270
|
};
|
|
206
271
|
|
|
207
272
|
/** The handler of a non-tool row, working-copy or not, or undefined. */
|
|
@@ -223,7 +288,7 @@ function dispatchable(command) {
|
|
|
223
288
|
return row && DISPATCHABLE.includes(row) ? row : undefined;
|
|
224
289
|
}
|
|
225
290
|
|
|
226
|
-
const USAGE = `juno — operate a Juno instance, and develop flows and
|
|
291
|
+
const USAGE = `juno — operate a Juno instance, and develop flows, integrations and apps for it
|
|
227
292
|
|
|
228
293
|
Usage: juno <command> [options]
|
|
229
294
|
juno <group> <verb> [id] [--input <json>|-|@file] [--jq <expr>]
|
|
@@ -239,6 +304,8 @@ ${workingCopyListing(DISPATCHABLE, handlerFor)}
|
|
|
239
304
|
|
|
240
305
|
--url <url> picks the instance; else $JUNO_URL, package.json juno.target,
|
|
241
306
|
then the user config (juno config set url <url>).
|
|
307
|
+
--token <key> is the API key to send; else $JUNO_TOKEN, then the user config
|
|
308
|
+
(juno config set token <key>). Create one under Settings → API keys.
|
|
242
309
|
`;
|
|
243
310
|
|
|
244
311
|
/**
|
|
@@ -281,11 +348,11 @@ export async function run(argv, io = defaultIO()) {
|
|
|
281
348
|
/**
|
|
282
349
|
* dispatch is two-level: the first word is a top-level row (`discover`) or a
|
|
283
350
|
* group, and a group's second word is the verb. A
|
|
284
|
-
* leading `--url` is accepted before the first word, so
|
|
351
|
+
* leading `--url` or `--token` is accepted before the first word, so
|
|
285
352
|
* `juno --url <u> state get x` reads the way an alias would write it.
|
|
286
353
|
*/
|
|
287
354
|
async function dispatch(argv, io) {
|
|
288
|
-
const {
|
|
355
|
+
const { leading, rest } = leadingFlags(argv);
|
|
289
356
|
const [first, ...tail] = rest;
|
|
290
357
|
|
|
291
358
|
if (!first || first === '--help' || first === '-h' || first === 'help') {
|
|
@@ -294,7 +361,7 @@ async function dispatch(argv, io) {
|
|
|
294
361
|
}
|
|
295
362
|
|
|
296
363
|
const top = dispatchable(first);
|
|
297
|
-
if (top) return runRow(top, tail,
|
|
364
|
+
if (top) return runRow(top, tail, leading, io);
|
|
298
365
|
|
|
299
366
|
if (GROUPS.has(first)) {
|
|
300
367
|
const [verb, ...args] = tail;
|
|
@@ -304,7 +371,7 @@ async function dispatch(argv, io) {
|
|
|
304
371
|
return runCommand(
|
|
305
372
|
{ options: URL_OPTION },
|
|
306
373
|
tail,
|
|
307
|
-
|
|
374
|
+
leading,
|
|
308
375
|
io,
|
|
309
376
|
(parsed) => showGroupHelp(first, parsed, io, EXIT_OK),
|
|
310
377
|
(parsed) => showGroupHelp(first, parsed, io, EXIT_USAGE)
|
|
@@ -316,7 +383,7 @@ async function dispatch(argv, io) {
|
|
|
316
383
|
`${first} ${normalizeVerb(verb)}`,
|
|
317
384
|
]);
|
|
318
385
|
}
|
|
319
|
-
return runRow(row, args,
|
|
386
|
+
return runRow(row, args, leading, io);
|
|
320
387
|
}
|
|
321
388
|
|
|
322
389
|
const second = tail[0] && !tail[0].startsWith('-') ? tail[0] : null;
|
|
@@ -335,7 +402,7 @@ async function showGroupHelp(group, args, io, code) {
|
|
|
335
402
|
}
|
|
336
403
|
|
|
337
404
|
/** runRow runs one verbs.json row, a tool's or a non-tool command's. */
|
|
338
|
-
function runRow(row, argv,
|
|
405
|
+
function runRow(row, argv, leading, io) {
|
|
339
406
|
if (!row.tool) {
|
|
340
407
|
const handler = handlerFor(row);
|
|
341
408
|
const run =
|
|
@@ -348,7 +415,7 @@ function runRow(row, argv, url, io) {
|
|
|
348
415
|
return runCommand(
|
|
349
416
|
handler,
|
|
350
417
|
argv,
|
|
351
|
-
|
|
418
|
+
leading,
|
|
352
419
|
io,
|
|
353
420
|
() => {
|
|
354
421
|
io.log(nonToolHelp(row, handler));
|
|
@@ -369,7 +436,7 @@ function runRow(row, argv, url, io) {
|
|
|
369
436
|
return runCommand(
|
|
370
437
|
{ run: (args) => runTool(row, args, io), options },
|
|
371
438
|
argv,
|
|
372
|
-
|
|
439
|
+
leading,
|
|
373
440
|
io,
|
|
374
441
|
async (args) => {
|
|
375
442
|
io.log(await verbHelp(row, args, io));
|
|
@@ -381,10 +448,10 @@ function runRow(row, argv, url, io) {
|
|
|
381
448
|
/**
|
|
382
449
|
* runCommand parses a command's own flags (plus --help) and runs it, or its
|
|
383
450
|
* help. A `--url` given before the command word fills in when the command's
|
|
384
|
-
* own is absent
|
|
451
|
+
* own is absent, and likewise `--token`. `run` defaults to the command's own; a group without a verb
|
|
385
452
|
* passes its help instead.
|
|
386
453
|
*/
|
|
387
|
-
async function runCommand(command, argv,
|
|
454
|
+
async function runCommand(command, argv, leading, io, help, run = command.run) {
|
|
388
455
|
let parsed;
|
|
389
456
|
try {
|
|
390
457
|
parsed = parseArgs({
|
|
@@ -400,7 +467,9 @@ async function runCommand(command, argv, url, io, help, run = command.run) {
|
|
|
400
467
|
}
|
|
401
468
|
|
|
402
469
|
const args = { ...parsed.values, _: parsed.positionals, client: io.client };
|
|
403
|
-
|
|
470
|
+
for (const [flag, value] of Object.entries(leading)) {
|
|
471
|
+
if (value !== undefined && args[flag] === undefined) args[flag] = value;
|
|
472
|
+
}
|
|
404
473
|
if (parsed.values.help) return help(args);
|
|
405
474
|
return (await run(args, io)) ?? EXIT_OK;
|
|
406
475
|
}
|
|
@@ -422,22 +491,29 @@ function refusePositional(row, args) {
|
|
|
422
491
|
);
|
|
423
492
|
}
|
|
424
493
|
|
|
425
|
-
/**
|
|
426
|
-
|
|
427
|
-
|
|
494
|
+
/**
|
|
495
|
+
* leadingFlags takes `--url <u>` / `--url=<u>` and `--token <k>` /
|
|
496
|
+
* `--token=<k>` off the front of argv, in either order.
|
|
497
|
+
*/
|
|
498
|
+
function leadingFlags(argv) {
|
|
499
|
+
const leading = {};
|
|
428
500
|
let i = 0;
|
|
429
501
|
while (i < argv.length) {
|
|
430
|
-
|
|
431
|
-
|
|
502
|
+
const flag = ['url', 'token'].find(
|
|
503
|
+
(name) =>
|
|
504
|
+
(argv[i] === `--${name}` && i + 1 < argv.length) ||
|
|
505
|
+
argv[i].startsWith(`--${name}=`)
|
|
506
|
+
);
|
|
507
|
+
if (!flag) break;
|
|
508
|
+
if (argv[i] === `--${flag}`) {
|
|
509
|
+
leading[flag] = argv[i + 1];
|
|
432
510
|
i += 2;
|
|
433
|
-
} else if (argv[i].startsWith('--url=')) {
|
|
434
|
-
url = argv[i].slice('--url='.length);
|
|
435
|
-
i += 1;
|
|
436
511
|
} else {
|
|
437
|
-
|
|
512
|
+
leading[flag] = argv[i].slice(`--${flag}=`.length);
|
|
513
|
+
i += 1;
|
|
438
514
|
}
|
|
439
515
|
}
|
|
440
|
-
return {
|
|
516
|
+
return { leading, rest: argv.slice(i) };
|
|
441
517
|
}
|
|
442
518
|
|
|
443
519
|
/**
|
package/src/project.js
CHANGED
|
@@ -2,15 +2,22 @@ import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { dirname, join, posix, relative, sep } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { UserError } from './errors.js';
|
|
5
|
+
import { PACKAGE_JSON, writePackageJSON } from './working-copy.js';
|
|
5
6
|
|
|
6
7
|
/** Only these reach the server. Everything else is project furniture. */
|
|
7
8
|
const SOURCE_EXTENSIONS = ['.ts', '.json'];
|
|
8
9
|
const SOURCE_DIR = 'src';
|
|
10
|
+
const TEST_DIR = 'test';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* Read the four things Juno looks at: manifest.json, the sources under src
|
|
12
|
-
* package.json's dependencies, and the lockfile.
|
|
13
|
-
*
|
|
13
|
+
* Read the four things Juno looks at: manifest.json, the sources under src/
|
|
14
|
+
* and test/, package.json's dependencies, and the lockfile. test/ is stored
|
|
15
|
+
* with the version but never compiled, linted or run server-side; types/ is
|
|
16
|
+
* generated and never uploaded.
|
|
17
|
+
*
|
|
18
|
+
* `baseVersion` is package.json's `juno.base_version`, the version this copy
|
|
19
|
+
* was pulled from or last deployed as, or undefined when it records none (a
|
|
20
|
+
* copy made before the check existed, or one with no `juno` block at all).
|
|
14
21
|
*/
|
|
15
22
|
export async function readProject(cwd = process.cwd()) {
|
|
16
23
|
const manifest = await readManifest(cwd);
|
|
@@ -18,14 +25,61 @@ export async function readProject(cwd = process.cwd()) {
|
|
|
18
25
|
if (Object.keys(files).length === 0) {
|
|
19
26
|
throw new UserError(`no TypeScript sources found under ${SOURCE_DIR}/`);
|
|
20
27
|
}
|
|
28
|
+
const packageJSON = await readOptional(join(cwd, PACKAGE_JSON));
|
|
21
29
|
return {
|
|
22
30
|
manifest,
|
|
23
|
-
files,
|
|
24
|
-
packageJSON
|
|
31
|
+
files: { ...files, ...(await readTestFiles(cwd)) },
|
|
32
|
+
packageJSON,
|
|
25
33
|
lockfile: await readOptional(join(cwd, 'package-lock.json')),
|
|
34
|
+
baseVersion: baseVersionOfText(packageJSON),
|
|
26
35
|
};
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
/**
|
|
39
|
+
* readIntegrationBaseVersion is package.json's `juno.base_version`, or
|
|
40
|
+
* undefined: what `pull` diffs against without reading the sources.
|
|
41
|
+
*/
|
|
42
|
+
export async function readIntegrationBaseVersion(cwd) {
|
|
43
|
+
return baseVersionOfText(await readOptional(join(cwd, PACKAGE_JSON)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `juno.base_version` of package.json's text, or undefined. */
|
|
47
|
+
function baseVersionOfText(text) {
|
|
48
|
+
let pkg;
|
|
49
|
+
try {
|
|
50
|
+
pkg = JSON.parse(text);
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const base = pkg?.juno?.base_version;
|
|
55
|
+
return Number.isInteger(base) ? base : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* updateIntegrationPackage rewrites package.json through `mutate`, keeping
|
|
60
|
+
* every key it does not touch (and their order). `juno` is handed over as an
|
|
61
|
+
* object even when the copy has none yet: an integration copy carries one only
|
|
62
|
+
* once it records a target or a base. A missing or unreadable package.json is
|
|
63
|
+
* left alone with a warning — no command here is the place to invent one.
|
|
64
|
+
* Returns whether it wrote.
|
|
65
|
+
*/
|
|
66
|
+
export async function updateIntegrationPackage(cwd, mutate, io) {
|
|
67
|
+
let pkg;
|
|
68
|
+
try {
|
|
69
|
+
pkg = JSON.parse(await readFile(join(cwd, PACKAGE_JSON), 'utf8'));
|
|
70
|
+
} catch {
|
|
71
|
+
pkg = null;
|
|
72
|
+
}
|
|
73
|
+
if (pkg === null || typeof pkg !== 'object' || Array.isArray(pkg)) {
|
|
74
|
+
io.warn(`no readable ${PACKAGE_JSON} — it was not updated`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (pkg.juno === null || typeof pkg.juno !== 'object') pkg.juno = {};
|
|
78
|
+
mutate(pkg);
|
|
79
|
+
await writePackageJSON(cwd, pkg);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
29
83
|
/**
|
|
30
84
|
* The manifest as it sits on disk. The stamp in types/integration-env.d.ts
|
|
31
85
|
* fingerprints these exact bytes, so both writing and verifying it go through
|
|
@@ -51,15 +105,29 @@ export async function readManifest(cwd = process.cwd()) {
|
|
|
51
105
|
}
|
|
52
106
|
|
|
53
107
|
/** Walks src/ and returns repo-relative POSIX paths → content. */
|
|
54
|
-
export
|
|
55
|
-
|
|
108
|
+
export function readSourceFiles(cwd = process.cwd()) {
|
|
109
|
+
return readSourceTree(cwd, SOURCE_DIR);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* readTestFiles is readSourceFiles for test/: the same .ts and .json rule,
|
|
114
|
+
* because Juno stores tests beside the sources under the same path rules.
|
|
115
|
+
* Every kind's working copy sends them; none requires them.
|
|
116
|
+
*/
|
|
117
|
+
export function readTestFiles(cwd = process.cwd()) {
|
|
118
|
+
return readSourceTree(cwd, TEST_DIR);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** readSourceTree walks dir for .ts and .json files, by repo-relative POSIX path. */
|
|
122
|
+
async function readSourceTree(cwd, dir) {
|
|
123
|
+
const root = join(cwd, dir);
|
|
56
124
|
const files = {};
|
|
57
125
|
async function walk(dir) {
|
|
58
126
|
let entries;
|
|
59
127
|
try {
|
|
60
128
|
entries = await readdir(dir, { withFileTypes: true });
|
|
61
129
|
} catch {
|
|
62
|
-
return; // no
|
|
130
|
+
return; // no such directory at all; the caller reports a missing src/
|
|
63
131
|
}
|
|
64
132
|
for (const entry of entries) {
|
|
65
133
|
const full = join(dir, entry.name);
|
package/src/token.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readUserConfig } from './config.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the API key to send as `Authorization: Bearer`, in precedence
|
|
5
|
+
* order: an explicit --token, then $JUNO_TOKEN, then `token` in the user
|
|
6
|
+
* config file `juno config set token` writes. It is the order resolveTarget
|
|
7
|
+
* follows for the URL, minus package.json: a working copy's package.json is
|
|
8
|
+
* committed, and a secret never goes in a file that is.
|
|
9
|
+
*
|
|
10
|
+
* Returns undefined when there is none: a request goes out unauthenticated,
|
|
11
|
+
* which is right for an instance behind a proxy that identifies the person,
|
|
12
|
+
* or one started with --no-auth. A blank value counts as unset, so an empty
|
|
13
|
+
* $JUNO_TOKEN does not shadow the config file.
|
|
14
|
+
*
|
|
15
|
+
* The token is a secret. Nothing in this CLI prints it, in an error message
|
|
16
|
+
* included.
|
|
17
|
+
*/
|
|
18
|
+
export async function resolveToken({ token, env = process.env } = {}) {
|
|
19
|
+
return (
|
|
20
|
+
clean(token) ??
|
|
21
|
+
clean(env.JUNO_TOKEN) ??
|
|
22
|
+
clean((await readUserConfig(env)).token)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function clean(value) {
|
|
27
|
+
if (typeof value !== 'string') return undefined;
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
return trimmed === '' ? undefined : trimmed;
|
|
30
|
+
}
|
package/src/watch.js
CHANGED
|
@@ -28,23 +28,60 @@ export async function deployOnChange({ io, target, paths, once }) {
|
|
|
28
28
|
};
|
|
29
29
|
await iteration();
|
|
30
30
|
|
|
31
|
+
const watcher = watchPaths(paths, () => void iteration());
|
|
32
|
+
await new Promise((resolve) => {
|
|
33
|
+
process.once('SIGINT', () => {
|
|
34
|
+
watcher.close();
|
|
35
|
+
resolve();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* watchPaths calls onChange once edits to `paths` (`{path, recursive?,
|
|
43
|
+
* filter?}`) have settled for `debounceMs`, so an editor's save-and-rename is
|
|
44
|
+
* one call. `filter(filename)`, when given, drops an event its path does not
|
|
45
|
+
* want — the way to watch one file in a directory, since a watch on the file
|
|
46
|
+
* itself loses it the first time an editor saves by renaming over it. A path
|
|
47
|
+
* that does not exist is not watched: an app's public/ may be absent, since
|
|
48
|
+
* nothing but the page is required. Returns `{close()}`, which also drops a
|
|
49
|
+
* pending call.
|
|
50
|
+
*/
|
|
51
|
+
export function watchPaths(
|
|
52
|
+
paths,
|
|
53
|
+
onChange,
|
|
54
|
+
{ debounceMs = WATCH_DEBOUNCE_MS } = {}
|
|
55
|
+
) {
|
|
31
56
|
let timer = null;
|
|
32
57
|
const schedule = () => {
|
|
33
58
|
if (timer) clearTimeout(timer);
|
|
34
59
|
timer = setTimeout(() => {
|
|
35
60
|
timer = null;
|
|
36
|
-
|
|
37
|
-
},
|
|
61
|
+
onChange();
|
|
62
|
+
}, debounceMs);
|
|
38
63
|
};
|
|
39
64
|
|
|
40
|
-
const watchers =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
const watchers = [];
|
|
66
|
+
for (const { path, recursive = false, filter } of paths) {
|
|
67
|
+
try {
|
|
68
|
+
const w = watch(path, { recursive }, (_event, filename) => {
|
|
69
|
+
if (!filter || filter(filename == null ? null : String(filename))) {
|
|
70
|
+
schedule();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// A watched directory removed mid-session is not worth a crash.
|
|
74
|
+
w.on('error', () => w.close());
|
|
75
|
+
watchers.push(w);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err.code !== 'ENOENT') throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
close() {
|
|
82
|
+
if (timer) clearTimeout(timer);
|
|
83
|
+
timer = null;
|
|
45
84
|
for (const w of watchers) w.close();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
49
|
-
return 0;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
50
87
|
}
|