@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/commands/init.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { mkdir, writeFile, access } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join, resolve, sep } from 'node:path';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { connect } from '../api.js';
|
|
5
5
|
import { printAuthoringIndex } from '../authoring-skills.js';
|
|
6
6
|
import { UsageError, UserError } from '../errors.js';
|
|
7
|
-
import { resolveTarget } from '../target.js';
|
|
8
7
|
|
|
9
8
|
export const INIT_USAGE = 'juno integration init <id> [--name <name>] [--here]';
|
|
10
9
|
|
|
@@ -29,13 +28,7 @@ export async function init(argv, io) {
|
|
|
29
28
|
throw new UserError(`${dir} already exists`);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
const target = await
|
|
33
|
-
url: argv.url,
|
|
34
|
-
cwd: io.cwd,
|
|
35
|
-
env: io.env,
|
|
36
|
-
});
|
|
37
|
-
const client =
|
|
38
|
-
argv.client ?? new JunoClient(target, { client: clientName(io.env) });
|
|
31
|
+
const { target, client } = await connect(argv, io);
|
|
39
32
|
// The id is validated server-side, against the same rules an install is:
|
|
40
33
|
// one answer to "is this a valid integration id" rather than two.
|
|
41
34
|
const files = await client.scaffold(id, { name: argv.name, target });
|
package/src/commands/pull.js
CHANGED
|
@@ -1,143 +1,130 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
|
|
6
|
-
import { clientName, JunoClient } from '../api.js';
|
|
1
|
+
import { connect } from '../api.js';
|
|
7
2
|
import { UserError } from '../errors.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
import {
|
|
4
|
+
readIntegrationBaseVersion,
|
|
5
|
+
readManifest,
|
|
6
|
+
updateIntegrationPackage,
|
|
7
|
+
} from '../project.js';
|
|
8
|
+
import { stampTypeFiles } from '../stamp.js';
|
|
9
|
+
import {
|
|
10
|
+
baseArtifactPaths,
|
|
11
|
+
MANIFEST,
|
|
12
|
+
manifestText,
|
|
13
|
+
pullArtifacts,
|
|
14
|
+
requireCleanTree,
|
|
15
|
+
setDependencies,
|
|
16
|
+
writeChangedFiles,
|
|
17
|
+
} from '../working-copy.js';
|
|
18
|
+
import { pickTypeFiles } from './gen-types.js';
|
|
12
19
|
|
|
13
20
|
export const PULL_USAGE = 'juno integration pull [--force] [--id <id>]';
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
|
-
* `juno integration pull` —
|
|
17
|
-
*
|
|
23
|
+
* `juno integration pull` — make the working copy's artifact files the
|
|
24
|
+
* install's active version: manifest.json and every file under src/ and
|
|
25
|
+
* test/, exactly as the version stores them, plus types/ regenerated and
|
|
26
|
+
* stamped against the pulled manifest, and package.json's
|
|
27
|
+
* `juno.base_version` (the version pulled, which the next check and deploy
|
|
28
|
+
* send).
|
|
18
29
|
*
|
|
19
30
|
* This exists because the zip is a good first touch and a bad second one: once
|
|
20
31
|
* the authoring agent has edited an integration you deployed, downloading the
|
|
21
32
|
* project again would clobber your working tree, while pull touches only the
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
33
|
+
* files Juno stores. The scaffold's script lines are not among them: they are
|
|
34
|
+
* project content, and an author's own edits to them are theirs — a fresh
|
|
35
|
+
* `juno integration init` is how to get the current ones. `dependencies` is
|
|
36
|
+
* replaced only when the pulled manifest declares some (an agent-authored
|
|
37
|
+
* integration); otherwise they live in package.json alone, which Juno does not
|
|
38
|
+
* store, and stay. Every other key in package.json, and of `juno`, stays.
|
|
39
|
+
*
|
|
40
|
+
* A local file is deleted only when the base version had it and the pulled
|
|
41
|
+
* one does not; a file neither has is one not deployed yet, and stays. It
|
|
42
|
+
* still refuses a dirty tree without --force, because an uncommitted edit is
|
|
43
|
+
* the one thing git cannot get back for you.
|
|
28
44
|
*/
|
|
29
45
|
export async function pull(argv, io) {
|
|
30
|
-
const target = await
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
})
|
|
35
|
-
|
|
46
|
+
const { target, client } = await connect(argv, io);
|
|
47
|
+
let manifestId;
|
|
48
|
+
try {
|
|
49
|
+
manifestId = (await readManifest(io.cwd)).id;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
// With --id the manifest is about to be replaced, so a broken one is
|
|
52
|
+
// no reason to refuse.
|
|
53
|
+
if (argv.id === undefined) throw err;
|
|
54
|
+
}
|
|
55
|
+
const id = argv.id ?? manifestId;
|
|
36
56
|
if (!id) {
|
|
37
57
|
throw new UserError(
|
|
38
58
|
'manifest.json declares no id — pass --id <integration>'
|
|
39
59
|
);
|
|
40
60
|
}
|
|
41
61
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
);
|
|
48
|
-
} else if (dirty.length > 0) {
|
|
49
|
-
throw new UserError(
|
|
50
|
-
`working tree has uncommitted changes:\n${dirty.map((p) => ` ${p}`).join('\n')}\n` +
|
|
51
|
-
'Commit or stash them, or pass --force to overwrite.'
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
62
|
+
await requireCleanTree(io.cwd, {
|
|
63
|
+
force: argv.force,
|
|
64
|
+
overwrites: `${MANIFEST}, src/, test/ and types/`,
|
|
65
|
+
io,
|
|
66
|
+
});
|
|
55
67
|
|
|
56
|
-
const client =
|
|
57
|
-
argv.client ?? new JunoClient(target, { client: clientName(io.env) });
|
|
58
68
|
const detail = await client.getIntegration(id);
|
|
59
69
|
if (!detail?.manifest) {
|
|
60
70
|
throw new UserError(
|
|
61
71
|
`${id} has no active version on ${target} — nothing to pull`
|
|
62
72
|
);
|
|
63
73
|
}
|
|
64
|
-
//
|
|
65
|
-
// copy as it was.
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
// Everything is fetched before anything is written, so a failure leaves
|
|
75
|
+
// the working copy as it was.
|
|
76
|
+
const manifestRaw = manifestText(detail.manifest);
|
|
77
|
+
const stored = { [MANIFEST]: manifestRaw, ...detail.source_files };
|
|
78
|
+
const types = stampTypeFiles(
|
|
79
|
+
pickTypeFiles(await client.typegen(detail.manifest)),
|
|
80
|
+
manifestRaw
|
|
69
81
|
);
|
|
82
|
+
// The base belongs to the install the manifest names; pulling another
|
|
83
|
+
// (--id) has no base to diff against, so it deletes nothing.
|
|
84
|
+
const base = await baseArtifactPaths({
|
|
85
|
+
base:
|
|
86
|
+
id === manifestId ? await readIntegrationBaseVersion(io.cwd) : undefined,
|
|
87
|
+
version: detail.version,
|
|
88
|
+
stored,
|
|
89
|
+
read: async (v) => {
|
|
90
|
+
const b = await client.getIntegrationVersion(id, v);
|
|
91
|
+
return b ? [MANIFEST, ...Object.keys(b.SourceFiles ?? {})] : null;
|
|
92
|
+
},
|
|
93
|
+
id,
|
|
94
|
+
target,
|
|
95
|
+
io,
|
|
96
|
+
});
|
|
70
97
|
|
|
71
|
-
const written =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
const { written, removed } = await pullArtifacts(io.cwd, {
|
|
99
|
+
stored,
|
|
100
|
+
base,
|
|
101
|
+
kind: 'integration',
|
|
102
|
+
});
|
|
103
|
+
written.push(
|
|
104
|
+
...(await writeChangedFiles(
|
|
105
|
+
io.cwd,
|
|
106
|
+
Object.fromEntries(
|
|
107
|
+
Object.entries(types).map(([name, content]) => [
|
|
108
|
+
`types/${name}`,
|
|
109
|
+
content,
|
|
110
|
+
])
|
|
111
|
+
)
|
|
112
|
+
))
|
|
76
113
|
);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
114
|
+
const wrote = await updateIntegrationPackage(
|
|
115
|
+
io.cwd,
|
|
116
|
+
(pkg) => {
|
|
117
|
+
if (Object.keys(detail.manifest.dependencies ?? {}).length) {
|
|
118
|
+
setDependencies(pkg, detail.manifest);
|
|
119
|
+
}
|
|
120
|
+
pkg.juno.base_version = detail.version;
|
|
121
|
+
},
|
|
122
|
+
io
|
|
86
123
|
);
|
|
124
|
+
if (wrote) written.push('package.json');
|
|
87
125
|
io.log(
|
|
88
|
-
|
|
126
|
+
`pulled ${id} v${detail.version} from ${target} (${written.sort().join(', ')})`
|
|
89
127
|
);
|
|
128
|
+
if (removed.length) io.log(`removed ${removed.join(', ')}`);
|
|
90
129
|
return 0;
|
|
91
130
|
}
|
|
92
|
-
|
|
93
|
-
/** The `scripts` of the package.json a scaffold answered. */
|
|
94
|
-
function scaffoldScripts(files, target) {
|
|
95
|
-
try {
|
|
96
|
-
const scripts = JSON.parse(files['package.json']).scripts;
|
|
97
|
-
if (scripts && typeof scripts === 'object') return scripts;
|
|
98
|
-
} catch {
|
|
99
|
-
// Reported below.
|
|
100
|
-
}
|
|
101
|
-
throw new UserError(
|
|
102
|
-
`${target} answered a project with no readable package.json scripts`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* replaceScripts sets package.json's `scripts`, keeping every other key and
|
|
108
|
-
* its order, written the way npm writes it. A missing or unreadable
|
|
109
|
-
* package.json is left alone with a warning: pull is not the place to invent
|
|
110
|
-
* one. Returns whether it wrote.
|
|
111
|
-
*/
|
|
112
|
-
async function replaceScripts(cwd, scripts, io) {
|
|
113
|
-
const path = join(cwd, 'package.json');
|
|
114
|
-
let pkg;
|
|
115
|
-
try {
|
|
116
|
-
pkg = JSON.parse(await readFile(path, 'utf8'));
|
|
117
|
-
} catch {
|
|
118
|
-
io.warn('no readable package.json — its scripts were not updated');
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
pkg.scripts = scripts;
|
|
122
|
-
await writeFile(path, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Returns the paths git reports as changed, [] for a clean tree, or null when
|
|
128
|
-
* this is not a git repository at all — three states the caller treats
|
|
129
|
-
* differently, so they must not collapse into a boolean.
|
|
130
|
-
*/
|
|
131
|
-
export async function dirtyPaths(cwd) {
|
|
132
|
-
try {
|
|
133
|
-
const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
|
|
134
|
-
cwd,
|
|
135
|
-
});
|
|
136
|
-
return stdout
|
|
137
|
-
.split('\n')
|
|
138
|
-
.map((line) => line.trim())
|
|
139
|
-
.filter(Boolean);
|
|
140
|
-
} catch {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
}
|
package/src/config.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The user config file: `$XDG_CONFIG_HOME/juno/config.json`, or
|
|
7
7
|
* `~/.config/juno/config.json` when XDG_CONFIG_HOME is unset. It holds
|
|
8
|
-
* `{"url": "…"}
|
|
9
|
-
* usable outside a project after a global install
|
|
8
|
+
* `{"url": "…", "token": "juno_…"}`: `url` is the last place resolveTarget
|
|
9
|
+
* looks, which makes `juno` usable outside a project after a global install,
|
|
10
|
+
* and `token` is the last place resolveToken looks. The token is a secret,
|
|
11
|
+
* so the file is mode 0600 (see writeUserConfig).
|
|
10
12
|
*/
|
|
11
13
|
export function configPath(env = process.env) {
|
|
12
14
|
const base = env.XDG_CONFIG_HOME || join(env.HOME || homedir(), '.config');
|
|
@@ -25,10 +27,23 @@ export async function readUserConfig(env = process.env) {
|
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Writes the whole config, creating its directory. The file is created 0600,
|
|
32
|
+
* and one that already exists is narrowed to 0600 before the new content
|
|
33
|
+
* goes in, so a token never sits in a file other users can read, not even
|
|
34
|
+
* for the length of one write. (`mode` alone only applies on creation.)
|
|
35
|
+
*/
|
|
29
36
|
export async function writeUserConfig(env = process.env, cfg) {
|
|
30
37
|
const path = configPath(env);
|
|
31
38
|
await mkdir(dirname(path), { recursive: true });
|
|
32
|
-
|
|
39
|
+
try {
|
|
40
|
+
await chmod(path, 0o600);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
if (err.code !== 'ENOENT') throw err;
|
|
43
|
+
}
|
|
44
|
+
await writeFile(path, `${JSON.stringify(cfg, null, 2)}\n`, {
|
|
45
|
+
encoding: 'utf8',
|
|
46
|
+
mode: 0o600,
|
|
47
|
+
});
|
|
33
48
|
return path;
|
|
34
49
|
}
|
package/src/errors.js
CHANGED
|
@@ -52,9 +52,9 @@ export class UnreachableError extends Error {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* ConflictError is a deploy the server refused because the working
|
|
56
|
-
* stale: the flow's active version is not the
|
|
57
|
-
* was pulled from (a 409). The top-level handler prints the server's message,
|
|
55
|
+
* ConflictError is a deploy or check the server refused because the working
|
|
56
|
+
* copy is stale: the flow's, integration's or app's active version is not the
|
|
57
|
+
* one `juno.base_version` says it was pulled from (a 409). The top-level handler prints the server's message,
|
|
58
58
|
* which names both versions, and exits 4. The fix is `npm run pull`, or
|
|
59
59
|
* deploying with --force to replace whatever is active.
|
|
60
60
|
*/
|
package/src/exit.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* 2 usage: unknown command or flag, bad --input, a gateway 422 or 404,
|
|
9
9
|
* a bad --jq
|
|
10
10
|
* 3 the instance is unreachable (the fetch itself failed)
|
|
11
|
-
* 4 a deploy conflict:
|
|
12
|
-
* active version is not the
|
|
11
|
+
* 4 a deploy conflict: a working-copy deploy or check (any kind) got a
|
|
12
|
+
* 409 because the active version is not the copy's base_version
|
|
13
13
|
* 5 `juno ask` / `session approve`: the turn is still running (pending)
|
|
14
14
|
* 6 `juno ask` / `session approve`: the assistant is awaiting input
|
|
15
15
|
*/
|
package/src/flow-project.js
CHANGED
|
@@ -1,128 +1,78 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
|
|
4
1
|
import { UserError } from './errors.js';
|
|
2
|
+
import { readSourceFiles, readTestFiles } from './project.js';
|
|
3
|
+
import {
|
|
4
|
+
baseVersionOf,
|
|
5
|
+
MANIFEST,
|
|
6
|
+
manifestText,
|
|
7
|
+
PACKAGE_JSON,
|
|
8
|
+
readBoundPackage,
|
|
9
|
+
readManifestFile,
|
|
10
|
+
typeFiles,
|
|
11
|
+
writeChangedFiles,
|
|
12
|
+
writePackageJSON,
|
|
13
|
+
} from './working-copy.js';
|
|
5
14
|
|
|
6
15
|
/**
|
|
7
|
-
* A flow working copy is
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* A flow working copy is laid out as an integration's is: manifest.json, the
|
|
17
|
+
* sources under src/ (entry src/flow.ts) and the tests under test/ are the
|
|
18
|
+
* author's, types/ is
|
|
19
|
+
* generated, and package.json's `juno` block binds the directory to one flow.
|
|
20
|
+
* The layout is the server's (POST /api/flows/scaffold); this module only
|
|
21
|
+
* knows the handful of names a command reads or writes back, and what it
|
|
22
|
+
* shares with an app working copy lives in working-copy.js.
|
|
12
23
|
*/
|
|
13
|
-
export
|
|
14
|
-
export const
|
|
15
|
-
export const
|
|
24
|
+
export { MANIFEST, PACKAGE_JSON, manifestText, typeFiles };
|
|
25
|
+
export const SRC_DIR = 'src';
|
|
26
|
+
export const FLOW_ENTRY = 'src/flow.ts';
|
|
16
27
|
|
|
17
28
|
/**
|
|
18
29
|
* readFlowProject reads what ingest takes: the manifest (parsed, so a broken
|
|
19
|
-
* one fails here with its parse error rather than as a server 422),
|
|
20
|
-
* and the binding in package.json.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
30
|
+
* one fails here with its parse error rather than as a server 422), every
|
|
31
|
+
* .ts and .json file under src/ and test/, and the binding in package.json.
|
|
32
|
+
* Juno stores test/ with the version and never compiles it. A copy
|
|
33
|
+
* with no src/flow.ts is refused before anything is sent. `flowId` is
|
|
34
|
+
* `juno.flow_id` — the id is identity, so the CLI sends it and the server
|
|
35
|
+
* refuses a manifest naming another — and `baseVersion` is
|
|
36
|
+
* `juno.base_version`, absent for a flow that has never been deployed from
|
|
37
|
+
* this copy.
|
|
24
38
|
*/
|
|
25
39
|
export async function readFlowProject(cwd) {
|
|
26
40
|
const pkg = await readFlowPackage(cwd);
|
|
41
|
+
const files = await readSourceFiles(cwd);
|
|
42
|
+
if (!Object.hasOwn(files, FLOW_ENTRY)) {
|
|
43
|
+
throw new UserError(
|
|
44
|
+
`no ${FLOW_ENTRY} — a flow working copy's entry is ${FLOW_ENTRY}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
27
47
|
return {
|
|
28
48
|
...(await readFlowManifest(cwd)),
|
|
29
|
-
|
|
49
|
+
files: { ...files, ...(await readTestFiles(cwd)) },
|
|
30
50
|
packageJSON: pkg,
|
|
31
51
|
flowId: pkg.juno.flow_id,
|
|
32
|
-
baseVersion:
|
|
33
|
-
? pkg.juno.base_version
|
|
34
|
-
: undefined,
|
|
52
|
+
baseVersion: baseVersionOf(pkg),
|
|
35
53
|
};
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
/** readFlowManifest is manifest.json, as bytes and parsed. */
|
|
39
|
-
export
|
|
40
|
-
const manifestRaw = await readText(cwd, MANIFEST);
|
|
41
|
-
try {
|
|
42
|
-
return { manifest: JSON.parse(manifestRaw), manifestRaw };
|
|
43
|
-
} catch (err) {
|
|
44
|
-
throw new UserError(`${MANIFEST} is not valid JSON: ${err.message}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
57
|
+
export const readFlowManifest = readManifestFile;
|
|
47
58
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
* running one from the wrong place should say so rather than fail on a
|
|
52
|
-
* missing flow.ts.
|
|
53
|
-
*/
|
|
54
|
-
export async function readFlowPackage(cwd) {
|
|
55
|
-
let pkg = null;
|
|
56
|
-
try {
|
|
57
|
-
pkg = JSON.parse(await readFile(join(cwd, PACKAGE_JSON), 'utf8'));
|
|
58
|
-
} catch {
|
|
59
|
-
// Missing or unreadable: reported below as not a working copy.
|
|
60
|
-
}
|
|
61
|
-
if (typeof pkg?.juno?.flow_id !== 'string' || pkg.juno.flow_id === '') {
|
|
62
|
-
throw new UserError(
|
|
63
|
-
`${cwd} is not a flow working copy (no juno.flow_id in package.json) — ` +
|
|
64
|
-
'run `juno flow init <id>` to make one'
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return pkg;
|
|
59
|
+
/** readFlowPackage is package.json, refusing a directory that is not a flow working copy. */
|
|
60
|
+
export function readFlowPackage(cwd) {
|
|
61
|
+
return readBoundPackage(cwd, { kind: 'flow', idKey: 'flow_id' });
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
/**
|
|
71
|
-
* updatePackageJSON rewrites package.json through
|
|
72
|
-
* it does not touch (and their order)
|
|
73
|
-
* newline — the way npm writes it, so the diff is only what changed.
|
|
65
|
+
* updatePackageJSON rewrites a flow working copy's package.json through
|
|
66
|
+
* `mutate`, keeping every key it does not touch (and their order).
|
|
74
67
|
*/
|
|
75
68
|
export async function updatePackageJSON(cwd, mutate) {
|
|
76
69
|
const pkg = await readFlowPackage(cwd);
|
|
77
70
|
mutate(pkg);
|
|
78
|
-
await
|
|
79
|
-
join(cwd, PACKAGE_JSON),
|
|
80
|
-
`${JSON.stringify(pkg, null, 2)}\n`,
|
|
81
|
-
'utf8'
|
|
82
|
-
);
|
|
71
|
+
await writePackageJSON(cwd, pkg);
|
|
83
72
|
}
|
|
84
73
|
|
|
85
74
|
/**
|
|
86
75
|
* writeFlowFiles writes files by working-copy name, skipping one whose
|
|
87
|
-
* content is already what is on disk
|
|
88
|
-
* changed nothing. Returns the names it wrote.
|
|
76
|
+
* content is already what is on disk. Returns the names it wrote.
|
|
89
77
|
*/
|
|
90
|
-
export
|
|
91
|
-
const written = [];
|
|
92
|
-
for (const [name, content] of Object.entries(files)) {
|
|
93
|
-
const path = join(cwd, name);
|
|
94
|
-
let current = null;
|
|
95
|
-
try {
|
|
96
|
-
current = await readFile(path, 'utf8');
|
|
97
|
-
} catch {
|
|
98
|
-
// Not there yet.
|
|
99
|
-
}
|
|
100
|
-
if (current === content) continue;
|
|
101
|
-
await writeFile(path, content, 'utf8');
|
|
102
|
-
written.push(name);
|
|
103
|
-
}
|
|
104
|
-
return written.sort();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The canonical manifest a route answered, as manifest.json holds it. */
|
|
108
|
-
export function manifestText(manifest) {
|
|
109
|
-
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* The .d.ts files of a route's `types`. Filtering by extension rather than
|
|
114
|
-
* naming the two keeps a future third file working without a CLI release.
|
|
115
|
-
*/
|
|
116
|
-
export function typeFiles(types) {
|
|
117
|
-
return Object.fromEntries(
|
|
118
|
-
Object.entries(types ?? {}).filter(([name]) => name.endsWith('.d.ts'))
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function readText(cwd, name) {
|
|
123
|
-
try {
|
|
124
|
-
return await readFile(join(cwd, name), 'utf8');
|
|
125
|
-
} catch {
|
|
126
|
-
throw new UserError(`no ${name} in ${cwd}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
78
|
+
export const writeFlowFiles = writeChangedFiles;
|