@junoflow/cli 0.3.0 → 0.9.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 +693 -52
- package/app-client.js +306 -0
- package/bin/juno.js +7 -0
- package/eslint.app.config.js +34 -0
- package/eslint.flow.config.js +35 -0
- package/package.json +12 -4
- package/src/api.js +541 -30
- 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 +43 -0
- package/src/commands/agent-skill-install.js +64 -0
- package/src/commands/app-check.js +75 -0
- package/src/commands/app-deploy.js +102 -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/ask.js +60 -0
- package/src/commands/check.js +71 -27
- package/src/commands/config.js +26 -0
- package/src/commands/credential-set.js +68 -0
- package/src/commands/deploy.js +95 -51
- package/src/commands/flow-check.js +75 -0
- package/src/commands/flow-deploy.js +110 -0
- package/src/commands/flow-fixture.js +67 -0
- package/src/commands/flow-gen-types.js +37 -0
- package/src/commands/flow-init.js +59 -0
- package/src/commands/flow-pull.js +115 -0
- package/src/commands/{types.js → gen-types.js} +12 -5
- package/src/commands/init.js +28 -16
- package/src/commands/pull.js +111 -63
- package/src/commands/session-approve.js +73 -0
- package/src/commands/skill.js +73 -0
- package/src/commands/tool.js +249 -0
- package/src/commands/verify-types.js +4 -2
- package/src/config.js +34 -0
- package/src/diagnostics.js +14 -0
- package/src/errors.js +40 -0
- package/src/exit.js +22 -0
- package/src/flow-project.js +78 -0
- package/src/help.js +199 -0
- package/src/index.js +567 -75
- package/src/installed-skill.js +55 -0
- package/src/project.js +76 -8
- package/src/prompt.js +196 -0
- package/src/stamp.js +10 -9
- package/src/suggest.js +67 -0
- package/src/target.js +22 -7
- package/src/verbs.js +34 -0
- package/src/watch.js +87 -0
- package/src/working-copy.js +262 -0
- package/tsconfig.page.json +15 -0
- package/verbs.json +276 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { UserError } from '../errors.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* loadFromWorkingCopy imports `name` as the working copy's own code would:
|
|
11
|
+
* resolved from its package.json, so out of its node_modules. The CLI has no
|
|
12
|
+
* runtime dependencies (`.claude/rules/cli.md`, "A thin client"), and the dev
|
|
13
|
+
* server must run the esbuild and testkit the project's own build and tests
|
|
14
|
+
* run, not a second copy of either.
|
|
15
|
+
*/
|
|
16
|
+
export async function loadFromWorkingCopy(cwd, name) {
|
|
17
|
+
let resolved;
|
|
18
|
+
try {
|
|
19
|
+
resolved = createRequire(join(cwd, 'package.json')).resolve(name);
|
|
20
|
+
} catch {
|
|
21
|
+
throw new UserError(
|
|
22
|
+
`${name} is not installed in this working copy — run npm install`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return import(pathToFileURL(resolved).href);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* createBuilder wraps the working copy's esbuild with the two builds the dev
|
|
30
|
+
* server makes: a page/ module for the browser, as ingest bundles it
|
|
31
|
+
* (internal/verify/page.go's bundlePage), and src/app.ts for Node, imported
|
|
32
|
+
* in process for the harness.
|
|
33
|
+
*
|
|
34
|
+
* A page module is built on every request, so a save needs no watcher to show
|
|
35
|
+
* up. The handlers are built once and kept: `invalidate()` marks them for a
|
|
36
|
+
* rebuild on the next `handlers()`, and a rebuild that fails keeps the last
|
|
37
|
+
* good module serving, with the failure in `error` — a syntax error mid-edit
|
|
38
|
+
* must not take the page's working calls down with it. `generation` counts
|
|
39
|
+
* the handler builds that succeeded, so a caller holding something built from
|
|
40
|
+
* one module can tell when it is out of date.
|
|
41
|
+
*/
|
|
42
|
+
export async function createBuilder(cwd) {
|
|
43
|
+
const mod = await loadFromWorkingCopy(cwd, 'esbuild');
|
|
44
|
+
const esbuild = typeof mod.build === 'function' ? mod : mod.default;
|
|
45
|
+
const tmp = await mkdtemp(join(tmpdir(), 'juno-app-dev-'));
|
|
46
|
+
|
|
47
|
+
let generation = 0;
|
|
48
|
+
let good = null; // the last module that built and imported
|
|
49
|
+
let dirty = true;
|
|
50
|
+
let error = null; // the last rebuild's failure, null after a success
|
|
51
|
+
|
|
52
|
+
const builder = {
|
|
53
|
+
/** The page/ module at abs, bundled for the browser, as JavaScript text. */
|
|
54
|
+
async page(abs) {
|
|
55
|
+
const out = await esbuild.build({
|
|
56
|
+
entryPoints: [abs],
|
|
57
|
+
absWorkingDir: cwd,
|
|
58
|
+
bundle: true,
|
|
59
|
+
format: 'esm',
|
|
60
|
+
platform: 'browser',
|
|
61
|
+
target: 'es2020',
|
|
62
|
+
write: false,
|
|
63
|
+
logLevel: 'silent',
|
|
64
|
+
});
|
|
65
|
+
return out.outputFiles[0].text;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The handlers module's default export, rebuilt first when invalidated.
|
|
70
|
+
* Throws only when nothing has ever built.
|
|
71
|
+
*/
|
|
72
|
+
async handlers() {
|
|
73
|
+
if (dirty) {
|
|
74
|
+
dirty = false;
|
|
75
|
+
try {
|
|
76
|
+
good = await buildHandlers();
|
|
77
|
+
error = null;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// Retried on the next call too, so a fix is picked up even before
|
|
80
|
+
// anything invalidates.
|
|
81
|
+
error = err;
|
|
82
|
+
dirty = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!good) throw error;
|
|
86
|
+
return good;
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/** Marks src/ as changed: the next handlers() rebuilds. */
|
|
90
|
+
invalidate() {
|
|
91
|
+
dirty = true;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/** The last handlers rebuild's failure, or null. */
|
|
95
|
+
get error() {
|
|
96
|
+
return error;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/** How many handler builds have succeeded. */
|
|
100
|
+
get generation() {
|
|
101
|
+
return generation;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/** Removes the temp directory the handler builds are written to. */
|
|
105
|
+
close() {
|
|
106
|
+
return rm(tmp, { recursive: true, force: true });
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* One Node build of src/app.ts, written under tmp and imported with a
|
|
112
|
+
* cache-busting query: Node caches a module by URL, and the same URL would
|
|
113
|
+
* answer the first build forever.
|
|
114
|
+
*/
|
|
115
|
+
async function buildHandlers() {
|
|
116
|
+
const out = await esbuild.build({
|
|
117
|
+
entryPoints: [join(cwd, 'src', 'app.ts')],
|
|
118
|
+
absWorkingDir: cwd,
|
|
119
|
+
bundle: true,
|
|
120
|
+
format: 'esm',
|
|
121
|
+
platform: 'node',
|
|
122
|
+
target: 'node20',
|
|
123
|
+
write: false,
|
|
124
|
+
logLevel: 'silent',
|
|
125
|
+
});
|
|
126
|
+
const file = join(tmp, 'app.mjs');
|
|
127
|
+
await writeFile(file, out.outputFiles[0].text, 'utf8');
|
|
128
|
+
generation += 1;
|
|
129
|
+
const mod = await import(`${pathToFileURL(file).href}?v=${generation}`);
|
|
130
|
+
return mod.default;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return builder;
|
|
134
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { UserError } from '../errors.js';
|
|
5
|
+
import { loadFromWorkingCopy } from './build.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* createDevHarness runs the app's handlers the way `npm test` does: through
|
|
9
|
+
* the working copy's own @junoflow/testkit appHarness, so the dev server is
|
|
10
|
+
* exactly as strict as the project's tests — an unanswered target is a
|
|
11
|
+
* refusal naming `binding.method`, not a silent `undefined`.
|
|
12
|
+
*
|
|
13
|
+
* The world comes from a scenario file, the shape `h.scenario` takes
|
|
14
|
+
* (plan-local-dev-6 decision 7). With none, the world is empty, and every
|
|
15
|
+
* call that asks it something fails visibly.
|
|
16
|
+
*
|
|
17
|
+
* One harness lives until the scenario or the manifest changes; then a fresh
|
|
18
|
+
* one is built and the scenario re-applied to it. A handlers rebuild keeps
|
|
19
|
+
* the harness: its `app` reads the builder's latest module on every call.
|
|
20
|
+
*/
|
|
21
|
+
export async function createDevHarness({ cwd, manifest, builder }) {
|
|
22
|
+
const { appHarness } = await loadFromWorkingCopy(cwd, '@junoflow/testkit');
|
|
23
|
+
if (typeof appHarness !== 'function') {
|
|
24
|
+
throw new UserError(
|
|
25
|
+
'@junoflow/testkit in this working copy has no appHarness — run npm install @junoflow/testkit@latest'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let latest = { handlers: {} };
|
|
30
|
+
const app = {
|
|
31
|
+
get handlers() {
|
|
32
|
+
return latest.handlers;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const dev = {
|
|
37
|
+
manifest,
|
|
38
|
+
/** The scenario in use, `{path, value}` (path relative to cwd), or null. */
|
|
39
|
+
scenario: null,
|
|
40
|
+
/** The testkit harness calls go through; replaced by a reset. */
|
|
41
|
+
h: null,
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reads and applies the scenario at path (relative to cwd), or the empty
|
|
45
|
+
* world for null. A file that is not a scenario fails here, naming it,
|
|
46
|
+
* and the scenario in use stays.
|
|
47
|
+
*/
|
|
48
|
+
async useScenario(path) {
|
|
49
|
+
if (path == null) {
|
|
50
|
+
dev.scenario = null;
|
|
51
|
+
} else {
|
|
52
|
+
const abs = isAbsolute(path) ? path : resolve(cwd, path);
|
|
53
|
+
const rel = relative(cwd, abs);
|
|
54
|
+
const value = await readScenario(abs, rel);
|
|
55
|
+
// Applied to a throwaway harness first, so a bad key is refused with
|
|
56
|
+
// the testkit's message before anything is replaced.
|
|
57
|
+
try {
|
|
58
|
+
appHarness({ manifest: dev.manifest, app }).scenario(value);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
throw new UserError(`${rel}: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
dev.scenario = { path: rel, value };
|
|
63
|
+
}
|
|
64
|
+
reset();
|
|
65
|
+
return dev;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/** Replaces the manifest (manifest.json was saved) and resets. */
|
|
69
|
+
setManifest(m) {
|
|
70
|
+
dev.manifest = m;
|
|
71
|
+
reset();
|
|
72
|
+
return dev;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Calls handler name with input, after picking up the latest handlers
|
|
77
|
+
* build. Resolves to the result after the JSON round trip; throws what
|
|
78
|
+
* the handler threw, the harness's refusal, or the build's failure.
|
|
79
|
+
*/
|
|
80
|
+
async call(name, input) {
|
|
81
|
+
// A module with no default export reaches the harness's own refusal.
|
|
82
|
+
latest = (await builder.handlers()) ?? {};
|
|
83
|
+
return dev.h.call(name, input);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function reset() {
|
|
88
|
+
dev.h = appHarness({ manifest: dev.manifest, app });
|
|
89
|
+
if (dev.scenario) dev.h.scenario(dev.scenario.value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
reset();
|
|
93
|
+
return dev;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function readScenario(abs, rel) {
|
|
97
|
+
let text;
|
|
98
|
+
try {
|
|
99
|
+
text = await readFile(abs, 'utf8');
|
|
100
|
+
} catch (err) {
|
|
101
|
+
throw new UserError(`cannot read scenario ${rel}: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(text);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
throw new UserError(`scenario ${rel} is not valid JSON: ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>juno app dev</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light dark;
|
|
10
|
+
--bg: #f6f6f4;
|
|
11
|
+
--fg: #1d1d1b;
|
|
12
|
+
--muted: #6b6b66;
|
|
13
|
+
--line: #d9d9d4;
|
|
14
|
+
--field: #ffffff;
|
|
15
|
+
--error-bg: #fbe9e7;
|
|
16
|
+
--error-fg: #8a1c0c;
|
|
17
|
+
}
|
|
18
|
+
@media (prefers-color-scheme: dark) {
|
|
19
|
+
:root {
|
|
20
|
+
--bg: #1c1c1a;
|
|
21
|
+
--fg: #e8e8e4;
|
|
22
|
+
--muted: #9a9a94;
|
|
23
|
+
--line: #3a3a36;
|
|
24
|
+
--field: #262624;
|
|
25
|
+
--error-bg: #3b1a14;
|
|
26
|
+
--error-fg: #ffb4a6;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
* {
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
}
|
|
32
|
+
html,
|
|
33
|
+
body {
|
|
34
|
+
margin: 0;
|
|
35
|
+
height: 100%;
|
|
36
|
+
}
|
|
37
|
+
body {
|
|
38
|
+
display: flex;
|
|
39
|
+
background: var(--bg);
|
|
40
|
+
color: var(--fg);
|
|
41
|
+
font:
|
|
42
|
+
13px/1.4 system-ui,
|
|
43
|
+
sans-serif;
|
|
44
|
+
}
|
|
45
|
+
aside {
|
|
46
|
+
width: 320px;
|
|
47
|
+
flex: none;
|
|
48
|
+
overflow-y: auto;
|
|
49
|
+
padding: 12px;
|
|
50
|
+
border-right: 1px solid var(--line);
|
|
51
|
+
}
|
|
52
|
+
main {
|
|
53
|
+
flex: 1;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
}
|
|
56
|
+
iframe {
|
|
57
|
+
border: 0;
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: 100%;
|
|
60
|
+
background: #fff;
|
|
61
|
+
}
|
|
62
|
+
h1 {
|
|
63
|
+
font-size: 14px;
|
|
64
|
+
margin: 0 0 4px;
|
|
65
|
+
}
|
|
66
|
+
h2 {
|
|
67
|
+
font-size: 11px;
|
|
68
|
+
text-transform: uppercase;
|
|
69
|
+
letter-spacing: 0.05em;
|
|
70
|
+
color: var(--muted);
|
|
71
|
+
margin: 16px 0 6px;
|
|
72
|
+
}
|
|
73
|
+
#status {
|
|
74
|
+
color: var(--muted);
|
|
75
|
+
}
|
|
76
|
+
#errors {
|
|
77
|
+
background: var(--error-bg);
|
|
78
|
+
color: var(--error-fg);
|
|
79
|
+
padding: 8px;
|
|
80
|
+
margin: 8px 0 0;
|
|
81
|
+
border-radius: 4px;
|
|
82
|
+
white-space: pre-wrap;
|
|
83
|
+
font:
|
|
84
|
+
12px/1.4 ui-monospace,
|
|
85
|
+
monospace;
|
|
86
|
+
}
|
|
87
|
+
#errors:empty {
|
|
88
|
+
display: none;
|
|
89
|
+
}
|
|
90
|
+
select,
|
|
91
|
+
input,
|
|
92
|
+
button {
|
|
93
|
+
font: inherit;
|
|
94
|
+
color: inherit;
|
|
95
|
+
}
|
|
96
|
+
select,
|
|
97
|
+
input {
|
|
98
|
+
background: var(--field);
|
|
99
|
+
border: 1px solid var(--line);
|
|
100
|
+
border-radius: 4px;
|
|
101
|
+
padding: 3px 6px;
|
|
102
|
+
}
|
|
103
|
+
select {
|
|
104
|
+
width: 100%;
|
|
105
|
+
}
|
|
106
|
+
button {
|
|
107
|
+
background: var(--field);
|
|
108
|
+
border: 1px solid var(--line);
|
|
109
|
+
border-radius: 4px;
|
|
110
|
+
padding: 3px 8px;
|
|
111
|
+
cursor: pointer;
|
|
112
|
+
}
|
|
113
|
+
.binding {
|
|
114
|
+
border-top: 1px solid var(--line);
|
|
115
|
+
padding: 6px 0;
|
|
116
|
+
}
|
|
117
|
+
.binding .name {
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
}
|
|
120
|
+
.binding .kind {
|
|
121
|
+
color: var(--muted);
|
|
122
|
+
font-size: 11px;
|
|
123
|
+
margin-left: 4px;
|
|
124
|
+
}
|
|
125
|
+
.binding code {
|
|
126
|
+
display: block;
|
|
127
|
+
font:
|
|
128
|
+
12px/1.4 ui-monospace,
|
|
129
|
+
monospace;
|
|
130
|
+
color: var(--muted);
|
|
131
|
+
overflow-wrap: anywhere;
|
|
132
|
+
margin: 2px 0 4px;
|
|
133
|
+
}
|
|
134
|
+
.binding form {
|
|
135
|
+
display: flex;
|
|
136
|
+
gap: 4px;
|
|
137
|
+
}
|
|
138
|
+
.binding input {
|
|
139
|
+
flex: 1;
|
|
140
|
+
min-width: 0;
|
|
141
|
+
}
|
|
142
|
+
.empty,
|
|
143
|
+
.hint {
|
|
144
|
+
color: var(--muted);
|
|
145
|
+
}
|
|
146
|
+
.hint {
|
|
147
|
+
font-size: 11px;
|
|
148
|
+
margin-top: 4px;
|
|
149
|
+
}
|
|
150
|
+
</style>
|
|
151
|
+
</head>
|
|
152
|
+
<body>
|
|
153
|
+
<aside>
|
|
154
|
+
<h1 id="title">juno app dev</h1>
|
|
155
|
+
<div id="status">connecting…</div>
|
|
156
|
+
<pre id="errors"></pre>
|
|
157
|
+
|
|
158
|
+
<h2>Scenario</h2>
|
|
159
|
+
<select id="scenario"></select>
|
|
160
|
+
|
|
161
|
+
<h2>States</h2>
|
|
162
|
+
<div id="states"></div>
|
|
163
|
+
<div class="hint">
|
|
164
|
+
A value is JSON; anything else is sent as a string.
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<h2>Controls</h2>
|
|
168
|
+
<div id="controls"></div>
|
|
169
|
+
<div class="hint">
|
|
170
|
+
A value is <code>{"value", "active", "expires_at"}</code>. Press sends a
|
|
171
|
+
button press.
|
|
172
|
+
</div>
|
|
173
|
+
</aside>
|
|
174
|
+
<main><iframe id="page" title="the app"></iframe></main>
|
|
175
|
+
|
|
176
|
+
<script>
|
|
177
|
+
// The panel for `juno app dev` (tools/npm/cli/src/app-dev/panel.js):
|
|
178
|
+
// it draws the server's `state` events and posts what the author does.
|
|
179
|
+
// It never touches the page in the frame beyond reloading it.
|
|
180
|
+
'use strict';
|
|
181
|
+
|
|
182
|
+
var $ = function (id) {
|
|
183
|
+
return document.getElementById(id);
|
|
184
|
+
};
|
|
185
|
+
var frame = $('page');
|
|
186
|
+
var shape = null; // the bindings drawn, as JSON
|
|
187
|
+
|
|
188
|
+
function post(path, body) {
|
|
189
|
+
return fetch('/_dev/' + path, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
192
|
+
body: JSON.stringify(body),
|
|
193
|
+
})
|
|
194
|
+
.then(function (res) {
|
|
195
|
+
return res.json().then(function (data) {
|
|
196
|
+
if (!res.ok) throw new Error(data.error || res.statusText);
|
|
197
|
+
return data;
|
|
198
|
+
});
|
|
199
|
+
})
|
|
200
|
+
.catch(function (err) {
|
|
201
|
+
alert(err.message);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Typed text is JSON when it parses, and a string when it does not, so
|
|
206
|
+
// `on` needs no quotes.
|
|
207
|
+
function parse(text) {
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(text);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return text;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function el(tag, cls, text) {
|
|
216
|
+
var n = document.createElement(tag);
|
|
217
|
+
if (cls) n.className = cls;
|
|
218
|
+
if (text != null) n.textContent = text;
|
|
219
|
+
return n;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function row(section, name, kind) {
|
|
223
|
+
var div = el('div', 'binding');
|
|
224
|
+
div.dataset.name = name;
|
|
225
|
+
var head = el('div');
|
|
226
|
+
head.appendChild(el('span', 'name', name));
|
|
227
|
+
if (kind) head.appendChild(el('span', 'kind', kind));
|
|
228
|
+
div.appendChild(head);
|
|
229
|
+
div.appendChild(el('code', 'value'));
|
|
230
|
+
var form = el('form');
|
|
231
|
+
var input = el('input');
|
|
232
|
+
input.placeholder = 'new value';
|
|
233
|
+
form.appendChild(input);
|
|
234
|
+
form.appendChild(el('button', null, 'set'));
|
|
235
|
+
if (section === 'controls') {
|
|
236
|
+
var press = el('button', null, 'press');
|
|
237
|
+
press.type = 'button';
|
|
238
|
+
press.onclick = function () {
|
|
239
|
+
post('press', { binding: name });
|
|
240
|
+
};
|
|
241
|
+
form.appendChild(press);
|
|
242
|
+
}
|
|
243
|
+
form.onsubmit = function (e) {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
var body = {};
|
|
246
|
+
body[section] = {};
|
|
247
|
+
body[section][name] = parse(input.value);
|
|
248
|
+
post('set', body).then(function (ok) {
|
|
249
|
+
if (ok) input.value = '';
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
div.appendChild(form);
|
|
253
|
+
return div;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function drawBindings(s) {
|
|
257
|
+
var next = JSON.stringify(s.bindings);
|
|
258
|
+
if (next === shape) return;
|
|
259
|
+
shape = next;
|
|
260
|
+
var states = $('states');
|
|
261
|
+
var controls = $('controls');
|
|
262
|
+
states.textContent = '';
|
|
263
|
+
controls.textContent = '';
|
|
264
|
+
s.bindings.states.forEach(function (b) {
|
|
265
|
+
states.appendChild(row('states', b.name, b.kind));
|
|
266
|
+
});
|
|
267
|
+
s.bindings.controls.forEach(function (name) {
|
|
268
|
+
controls.appendChild(row('controls', name));
|
|
269
|
+
});
|
|
270
|
+
if (!s.bindings.states.length) {
|
|
271
|
+
states.appendChild(el('div', 'empty', 'none declared'));
|
|
272
|
+
}
|
|
273
|
+
if (!s.bindings.controls.length) {
|
|
274
|
+
controls.appendChild(el('div', 'empty', 'none declared'));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function drawValues(values) {
|
|
279
|
+
['states', 'controls'].forEach(function (section) {
|
|
280
|
+
$(section)
|
|
281
|
+
.querySelectorAll('.binding')
|
|
282
|
+
.forEach(function (div) {
|
|
283
|
+
var v = values[section][div.dataset.name];
|
|
284
|
+
div.querySelector('.value').textContent = JSON.stringify(v);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function drawScenarios(s) {
|
|
290
|
+
var sel = $('scenario');
|
|
291
|
+
sel.textContent = '';
|
|
292
|
+
var none = el('option', null, '(none: an empty world)');
|
|
293
|
+
none.value = '';
|
|
294
|
+
sel.appendChild(none);
|
|
295
|
+
var paths = s.scenarios.slice();
|
|
296
|
+
// A --scenario outside test/fixtures/ is still the one in use.
|
|
297
|
+
if (s.scenario && paths.indexOf(s.scenario) < 0) paths.push(s.scenario);
|
|
298
|
+
paths.forEach(function (p) {
|
|
299
|
+
var o = el('option', null, p);
|
|
300
|
+
o.value = p;
|
|
301
|
+
if (s.scenarios.indexOf(p) < 0) o.disabled = true;
|
|
302
|
+
sel.appendChild(o);
|
|
303
|
+
});
|
|
304
|
+
sel.value = s.scenario || '';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
$('scenario').onchange = function (e) {
|
|
308
|
+
post('scenario', { path: e.target.value || null });
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
function draw(s) {
|
|
312
|
+
document.title = s.id + ' — juno app dev';
|
|
313
|
+
$('title').textContent = s.id;
|
|
314
|
+
if (!frame.getAttribute('src')) frame.src = '/' + s.id + '/';
|
|
315
|
+
$('errors').textContent = s.errors.join('\n\n');
|
|
316
|
+
drawScenarios(s);
|
|
317
|
+
drawBindings(s);
|
|
318
|
+
drawValues(s.values);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
var events = new EventSource('/_dev/events');
|
|
322
|
+
events.addEventListener('open', function () {
|
|
323
|
+
$('status').textContent = 'connected';
|
|
324
|
+
});
|
|
325
|
+
events.addEventListener('error', function () {
|
|
326
|
+
$('status').textContent = 'disconnected: is juno app dev running?';
|
|
327
|
+
});
|
|
328
|
+
events.addEventListener('state', function (e) {
|
|
329
|
+
draw(JSON.parse(e.data));
|
|
330
|
+
});
|
|
331
|
+
events.addEventListener('reload', function () {
|
|
332
|
+
frame.contentWindow.location.reload();
|
|
333
|
+
});
|
|
334
|
+
</script>
|
|
335
|
+
</body>
|
|
336
|
+
</html>
|