@ollie-shop/cli 1.4.1 → 1.6.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/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +12 -0
- package/dist/index.js +523 -16
- package/package.json +1 -1
- package/src/commands/help.tsx +15 -0
- package/src/commands/setup-cmd.ts +142 -0
- package/src/commands/start.tsx +68 -1
- package/src/index.tsx +2 -0
- package/src/utils/config.ts +76 -0
- package/src/utils/esbuild.ts +376 -20
package/package.json
CHANGED
package/src/commands/help.tsx
CHANGED
|
@@ -91,6 +91,12 @@ export function HelpCommand() {
|
|
|
91
91
|
</Box>
|
|
92
92
|
<Text>Write store/version IDs to ollie.json</Text>
|
|
93
93
|
</Box>
|
|
94
|
+
<Box>
|
|
95
|
+
<Box width={24}>
|
|
96
|
+
<Text color="green">setup</Text>
|
|
97
|
+
</Box>
|
|
98
|
+
<Text>Migrate legacy meta.json into the config components map</Text>
|
|
99
|
+
</Box>
|
|
94
100
|
<Box>
|
|
95
101
|
<Box width={24}>
|
|
96
102
|
<Text color="green">help</Text>
|
|
@@ -145,6 +151,14 @@ export function HelpCommand() {
|
|
|
145
151
|
</Box>
|
|
146
152
|
<Text>start: don't auto-open Studio (also honored via CI env)</Text>
|
|
147
153
|
</Box>
|
|
154
|
+
<Box>
|
|
155
|
+
<Box width={24}>
|
|
156
|
+
<Text color="yellow">--browser-logs</Text>
|
|
157
|
+
</Box>
|
|
158
|
+
<Text>
|
|
159
|
+
start: stream custom components' browser console to terminal
|
|
160
|
+
</Text>
|
|
161
|
+
</Box>
|
|
148
162
|
</Box>
|
|
149
163
|
|
|
150
164
|
<Box marginTop={1}>
|
|
@@ -154,6 +168,7 @@ export function HelpCommand() {
|
|
|
154
168
|
<Text dimColor>$ ollieshop login</Text>
|
|
155
169
|
<Text dimColor>$ ollieshop start --stage dev</Text>
|
|
156
170
|
<Text dimColor>$ ollieshop start --no-open</Text>
|
|
171
|
+
<Text dimColor>$ ollieshop start --browser-logs</Text>
|
|
157
172
|
<Text dimColor>$ ollieshop whoami -o json</Text>
|
|
158
173
|
<Text dimColor>$ ollieshop schema store.create</Text>
|
|
159
174
|
<Text dimColor>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { glob } from "glob";
|
|
4
|
+
import { upsertComponentEntry } from "../utils/config.js";
|
|
5
|
+
import { detectOutputFormat, outputResult } from "../utils/output.js";
|
|
6
|
+
import type { ParsedArgs } from "../utils/parse-args.js";
|
|
7
|
+
|
|
8
|
+
const META_FILE_RE = /^meta(?:\.([^.]+))?\.json$/;
|
|
9
|
+
|
|
10
|
+
function configFileForStage(stage?: string): string {
|
|
11
|
+
return !stage || stage === "prod" ? "ollie.json" : `ollie.${stage}.json`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface MigratedMeta {
|
|
15
|
+
component: string;
|
|
16
|
+
stage: string;
|
|
17
|
+
configFile: string;
|
|
18
|
+
metaFile: string;
|
|
19
|
+
id: string;
|
|
20
|
+
slot?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Migrates legacy per-folder meta.json / meta.{stage}.json into the config
|
|
25
|
+
* `components` map (meta.json -> ollie.json, meta.{stage}.json -> ollie.{stage}.json)
|
|
26
|
+
* and removes the migrated meta files. Local-only metas (no id) are left in place.
|
|
27
|
+
*/
|
|
28
|
+
export async function setupCommand(parsed: ParsedArgs): Promise<void> {
|
|
29
|
+
const format = detectOutputFormat(parsed.global.output);
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
const dryRun = parsed.global.dryRun;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const componentsDir = path.join(cwd, "components");
|
|
35
|
+
let folders: string[] = [];
|
|
36
|
+
try {
|
|
37
|
+
await fs.access(componentsDir);
|
|
38
|
+
const entries = await glob("*/index.tsx", { cwd: componentsDir });
|
|
39
|
+
folders = entries.map((entry) => path.dirname(entry));
|
|
40
|
+
} catch {
|
|
41
|
+
// No components directory
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const migrated: MigratedMeta[] = [];
|
|
45
|
+
const skipped: Array<{
|
|
46
|
+
component: string;
|
|
47
|
+
metaFile: string;
|
|
48
|
+
reason: string;
|
|
49
|
+
}> = [];
|
|
50
|
+
|
|
51
|
+
for (const name of folders) {
|
|
52
|
+
const folderDir = path.join(componentsDir, name);
|
|
53
|
+
const metaFiles = await glob("meta*.json", { cwd: folderDir });
|
|
54
|
+
|
|
55
|
+
for (const metaFile of metaFiles) {
|
|
56
|
+
const match = metaFile.match(META_FILE_RE);
|
|
57
|
+
if (!match) continue;
|
|
58
|
+
const stage = match[1];
|
|
59
|
+
|
|
60
|
+
let meta: { id?: unknown; slot?: unknown };
|
|
61
|
+
try {
|
|
62
|
+
meta = JSON.parse(
|
|
63
|
+
await fs.readFile(path.join(folderDir, metaFile), "utf-8"),
|
|
64
|
+
);
|
|
65
|
+
} catch {
|
|
66
|
+
skipped.push({
|
|
67
|
+
component: name,
|
|
68
|
+
metaFile,
|
|
69
|
+
reason: "unreadable meta",
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const id = typeof meta.id === "string" ? meta.id : undefined;
|
|
75
|
+
if (!id) {
|
|
76
|
+
skipped.push({
|
|
77
|
+
component: name,
|
|
78
|
+
metaFile,
|
|
79
|
+
reason: "no id (unlinked) - left as-is",
|
|
80
|
+
});
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const slot = typeof meta.slot === "string" ? meta.slot : undefined;
|
|
84
|
+
|
|
85
|
+
if (!dryRun) {
|
|
86
|
+
await upsertComponentEntry(
|
|
87
|
+
id,
|
|
88
|
+
{ defaultPath: `components/${name}`, slot },
|
|
89
|
+
{ cwd, stage },
|
|
90
|
+
);
|
|
91
|
+
await fs.rm(path.join(folderDir, metaFile));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
migrated.push({
|
|
95
|
+
component: name,
|
|
96
|
+
stage: stage ?? "prod",
|
|
97
|
+
configFile: configFileForStage(stage),
|
|
98
|
+
metaFile,
|
|
99
|
+
id,
|
|
100
|
+
slot,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (format === "json") {
|
|
106
|
+
outputResult(
|
|
107
|
+
{ data: { dryRun, migrated, skipped } },
|
|
108
|
+
format,
|
|
109
|
+
parsed.global.fields,
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const prefix = dryRun ? "\x1b[33m[dry-run]\x1b[0m " : "";
|
|
115
|
+
if (migrated.length === 0) {
|
|
116
|
+
console.log(`${prefix}No legacy meta files to migrate.`);
|
|
117
|
+
} else {
|
|
118
|
+
console.log(
|
|
119
|
+
`${prefix}\x1b[1mMigrated ${migrated.length} meta file(s) to config:\x1b[0m`,
|
|
120
|
+
);
|
|
121
|
+
for (const entry of migrated) {
|
|
122
|
+
const slotLabel = entry.slot ? `, slot ${entry.slot}` : "";
|
|
123
|
+
const removed = dryRun ? "" : ` [removed ${entry.metaFile}]`;
|
|
124
|
+
console.log(
|
|
125
|
+
` ${entry.configFile.padEnd(16)} ${entry.component} (id ${entry.id}${slotLabel})${removed}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (skipped.length > 0) {
|
|
130
|
+
console.log("\n\x1b[2mSkipped:\x1b[0m");
|
|
131
|
+
for (const entry of skipped) {
|
|
132
|
+
console.log(` ${entry.component}/${entry.metaFile}: ${entry.reason}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
outputResult(
|
|
137
|
+
{ error: { message: err instanceof Error ? err.message : String(err) } },
|
|
138
|
+
format,
|
|
139
|
+
);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/commands/start.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import open from "open";
|
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
5
|
import { loadConfig, resolveStage } from "../utils/config.js";
|
|
6
6
|
import {
|
|
7
|
+
type BrowserLogEntry,
|
|
7
8
|
type ComponentInfo,
|
|
8
9
|
discoverComponents,
|
|
9
10
|
startDevServer,
|
|
@@ -22,6 +23,14 @@ interface RequestLog {
|
|
|
22
23
|
timestamp: Date;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
interface BrowserLog {
|
|
27
|
+
id: number;
|
|
28
|
+
level: string;
|
|
29
|
+
component?: string;
|
|
30
|
+
message: string;
|
|
31
|
+
timestamp: Date;
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
type ServerState =
|
|
26
35
|
| { status: "initializing" }
|
|
27
36
|
| { status: "discovering" }
|
|
@@ -51,9 +60,11 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
51
60
|
const [state, setState] = useState<ServerState>({ status: "initializing" });
|
|
52
61
|
const [components, setComponents] = useState<ComponentInfo[]>([]);
|
|
53
62
|
const [logs, setLogs] = useState<RequestLog[]>([]);
|
|
63
|
+
const [browserLogs, setBrowserLogs] = useState<BrowserLog[]>([]);
|
|
54
64
|
const [buildCount, setBuildCount] = useState(0);
|
|
55
65
|
const [lastBuildTime, setLastBuildTime] = useState<Date | null>(null);
|
|
56
66
|
const logIdRef = useRef(0);
|
|
67
|
+
const browserLogIdRef = useRef(0);
|
|
57
68
|
const rebuildRef = useRef<(() => Promise<void>) | null>(null);
|
|
58
69
|
const stopRef = useRef<(() => Promise<void>) | null>(null);
|
|
59
70
|
|
|
@@ -63,6 +74,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
63
74
|
// agent spawns the dev server and drives its own harness instead. Also honored
|
|
64
75
|
// via the conventional CI env var.
|
|
65
76
|
const noOpen = args.includes("--no-open") || Boolean(process.env.CI);
|
|
77
|
+
const browserLogsEnabled = args.includes("--browser-logs");
|
|
66
78
|
// Keyboard shortcuts need raw mode, which Ink can only enable on a TTY stdin.
|
|
67
79
|
// When spawned headless (agent/CI/backgrounded), stdin isn't a TTY — guard the
|
|
68
80
|
// input handler so the server runs instead of crashing with "Raw mode is not
|
|
@@ -92,6 +104,19 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
92
104
|
[addLog],
|
|
93
105
|
);
|
|
94
106
|
|
|
107
|
+
const addBrowserLogs = useCallback((entries: BrowserLogEntry[]) => {
|
|
108
|
+
if (entries.length === 0) return;
|
|
109
|
+
setBrowserLogs((prev) => {
|
|
110
|
+
const mapped = entries.map((entry) => ({
|
|
111
|
+
...entry,
|
|
112
|
+
message: sanitizeLogMessage(entry.message),
|
|
113
|
+
id: ++browserLogIdRef.current,
|
|
114
|
+
timestamp: new Date(),
|
|
115
|
+
}));
|
|
116
|
+
return [...prev, ...mapped].slice(-MAX_LOGS);
|
|
117
|
+
});
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
95
120
|
// Initialize server
|
|
96
121
|
useEffect(() => {
|
|
97
122
|
let mounted = true;
|
|
@@ -134,6 +159,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
134
159
|
port: PORT,
|
|
135
160
|
stage,
|
|
136
161
|
onRequest: handleRequest,
|
|
162
|
+
onBrowserLogs: browserLogsEnabled ? addBrowserLogs : undefined,
|
|
137
163
|
onBuildEnd: (updatedComponents) => {
|
|
138
164
|
setComponents(updatedComponents);
|
|
139
165
|
setBuildCount((c) => c + 1);
|
|
@@ -181,7 +207,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
181
207
|
mounted = false;
|
|
182
208
|
stopRef.current?.();
|
|
183
209
|
};
|
|
184
|
-
}, [stage, handleRequest, noOpen]);
|
|
210
|
+
}, [stage, handleRequest, addBrowserLogs, browserLogsEnabled, noOpen]);
|
|
185
211
|
|
|
186
212
|
// Handle keyboard input (only when attached to a TTY — see isInteractive above)
|
|
187
213
|
useInput(
|
|
@@ -255,6 +281,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
255
281
|
<ComponentList components={components} />
|
|
256
282
|
<BuildInfo buildCount={buildCount} lastBuildTime={lastBuildTime} />
|
|
257
283
|
<RequestLogs logs={logs} />
|
|
284
|
+
{browserLogsEnabled && <BrowserLogs logs={browserLogs} />}
|
|
258
285
|
<Footer interactive={isInteractive} />
|
|
259
286
|
</>
|
|
260
287
|
)}
|
|
@@ -404,6 +431,46 @@ function RequestLogs({ logs }: { logs: RequestLog[] }) {
|
|
|
404
431
|
);
|
|
405
432
|
}
|
|
406
433
|
|
|
434
|
+
function sanitizeLogMessage(message: string): string {
|
|
435
|
+
let out = "";
|
|
436
|
+
for (const ch of message) {
|
|
437
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
438
|
+
out += code < 0x20 || (code >= 0x7f && code <= 0x9f) ? " " : ch;
|
|
439
|
+
if (out.length >= 2000) break;
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function browserLogColor(level: string): string {
|
|
445
|
+
if (level === "error") return "red";
|
|
446
|
+
if (level === "warn") return "yellow";
|
|
447
|
+
if (level === "info") return "cyan";
|
|
448
|
+
return "gray";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function BrowserLogs({ logs }: { logs: BrowserLog[] }) {
|
|
452
|
+
return (
|
|
453
|
+
<Box marginTop={1} flexDirection="column">
|
|
454
|
+
<Text bold>Browser Logs:</Text>
|
|
455
|
+
<Box marginLeft={2} flexDirection="column">
|
|
456
|
+
{logs.length === 0 ? (
|
|
457
|
+
<Text dimColor>No browser logs yet...</Text>
|
|
458
|
+
) : (
|
|
459
|
+
logs.map((log) => (
|
|
460
|
+
<Box key={log.id}>
|
|
461
|
+
<Text color={browserLogColor(log.level)}>
|
|
462
|
+
{log.level.toUpperCase()}{" "}
|
|
463
|
+
</Text>
|
|
464
|
+
{log.component && <Text dimColor>[{log.component}] </Text>}
|
|
465
|
+
<Text>{log.message}</Text>
|
|
466
|
+
</Box>
|
|
467
|
+
))
|
|
468
|
+
)}
|
|
469
|
+
</Box>
|
|
470
|
+
</Box>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
407
474
|
function Footer({ interactive = true }: { interactive?: boolean }) {
|
|
408
475
|
if (!interactive) {
|
|
409
476
|
return (
|
package/src/index.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { deployCommand } from "./commands/deploy-cmd.js";
|
|
|
6
6
|
import { functionCommand } from "./commands/function-cmd.js";
|
|
7
7
|
import { initCommand } from "./commands/init-cmd.js";
|
|
8
8
|
import { schemaCommand } from "./commands/schema-cmd.js";
|
|
9
|
+
import { setupCommand } from "./commands/setup-cmd.js";
|
|
9
10
|
import { statusCommand } from "./commands/status-cmd.js";
|
|
10
11
|
import { storeCommand } from "./commands/store-cmd.js";
|
|
11
12
|
import { versionCommand } from "./commands/version-cmd.js";
|
|
@@ -27,6 +28,7 @@ const AGENT_COMMANDS: Record<
|
|
|
27
28
|
init: initCommand,
|
|
28
29
|
deploy: deployCommand,
|
|
29
30
|
status: statusCommand,
|
|
31
|
+
setup: setupCommand,
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
const parsed = parseArgs(process.argv);
|
package/src/utils/config.ts
CHANGED
|
@@ -2,10 +2,18 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
|
+
const ComponentEntrySchema = z.object({
|
|
6
|
+
path: z.string(),
|
|
7
|
+
slot: z.string().optional(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type ComponentEntry = z.infer<typeof ComponentEntrySchema>;
|
|
11
|
+
|
|
5
12
|
const OllieConfigSchema = z
|
|
6
13
|
.object({
|
|
7
14
|
storeId: z.string().uuid(),
|
|
8
15
|
versionId: z.string().uuid().optional(),
|
|
16
|
+
components: z.record(z.string(), ComponentEntrySchema).optional(),
|
|
9
17
|
})
|
|
10
18
|
.passthrough(); // Allow any other fields without validation
|
|
11
19
|
|
|
@@ -114,6 +122,74 @@ export async function saveConfig(
|
|
|
114
122
|
await fs.writeFile(configPath, JSON.stringify(merged, null, 2));
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Lists the stages available in a project by scanning its ollie config files.
|
|
127
|
+
* ollie.json is the "prod" stage; ollie.{stage}.json is the "{stage}" stage.
|
|
128
|
+
*/
|
|
129
|
+
export async function listStages(
|
|
130
|
+
cwd: string = process.cwd(),
|
|
131
|
+
): Promise<Array<{ stage: string; versionId?: string }>> {
|
|
132
|
+
let files: string[];
|
|
133
|
+
try {
|
|
134
|
+
files = await fs.readdir(cwd);
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const seen = new Set<string>();
|
|
140
|
+
const stages: Array<{ stage: string; versionId?: string }> = [];
|
|
141
|
+
for (const file of files.sort()) {
|
|
142
|
+
const match = file.match(/^ollie(?:\.([^.]+))?\.json$/);
|
|
143
|
+
if (!match) continue;
|
|
144
|
+
const stage = match[1] ?? "prod";
|
|
145
|
+
if (seen.has(stage)) continue;
|
|
146
|
+
seen.add(stage);
|
|
147
|
+
|
|
148
|
+
let versionId: string | undefined;
|
|
149
|
+
try {
|
|
150
|
+
const config = await loadConfig({ cwd, stage: match[1] });
|
|
151
|
+
versionId = config?.versionId;
|
|
152
|
+
} catch {
|
|
153
|
+
// List the stage even if its config is invalid/incomplete
|
|
154
|
+
}
|
|
155
|
+
stages.push({ stage, versionId });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return stages;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Upserts a component entry into the stage's config file (ollie.json or
|
|
163
|
+
* ollie.{stage}.json). Preserves the existing path/slot when not provided.
|
|
164
|
+
*/
|
|
165
|
+
export async function upsertComponentEntry(
|
|
166
|
+
id: string,
|
|
167
|
+
values: { defaultPath: string; slot?: string },
|
|
168
|
+
options: SaveConfigOptions = {},
|
|
169
|
+
): Promise<ComponentEntry> {
|
|
170
|
+
const { cwd = process.cwd(), stage } = options;
|
|
171
|
+
const configPath = path.join(cwd, getConfigFileName(stage));
|
|
172
|
+
|
|
173
|
+
const existing = (await loadConfigFile(configPath)) ?? {};
|
|
174
|
+
const components = {
|
|
175
|
+
...((existing.components as Record<string, ComponentEntry>) ?? {}),
|
|
176
|
+
};
|
|
177
|
+
const prev = components[id];
|
|
178
|
+
const slot = values.slot ?? prev?.slot;
|
|
179
|
+
const entry: ComponentEntry = {
|
|
180
|
+
path: prev?.path ?? values.defaultPath,
|
|
181
|
+
...(slot !== undefined ? { slot } : {}),
|
|
182
|
+
};
|
|
183
|
+
components[id] = entry;
|
|
184
|
+
|
|
185
|
+
await fs.writeFile(
|
|
186
|
+
configPath,
|
|
187
|
+
JSON.stringify({ ...existing, components }, null, 2),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return entry;
|
|
191
|
+
}
|
|
192
|
+
|
|
117
193
|
/**
|
|
118
194
|
* Returns the resolved stage from CLI args or environment.
|
|
119
195
|
* Priority: CLI arg > OLLIE_STAGE env > undefined (defaults to prod behavior)
|