@fire-spark/cli 0.0.0-stage → 0.13.3

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 (39) hide show
  1. package/bin/firespark.mjs +14 -0
  2. package/package.json +39 -3
  3. package/src/commands/doctor/client/admin.flow.ts +553 -0
  4. package/src/commands/doctor/client/catalog.json +1586 -0
  5. package/src/commands/doctor/client/catalog.ts +179 -0
  6. package/src/commands/doctor/client/checkout.flow.ts +400 -0
  7. package/src/commands/doctor/client/command.ts +246 -0
  8. package/src/commands/doctor/client/config.ts +346 -0
  9. package/src/commands/doctor/client/content.flow.ts +450 -0
  10. package/src/commands/doctor/client/copy.ts +248 -0
  11. package/src/commands/doctor/client/flow.ts +182 -0
  12. package/src/commands/doctor/client/log.flow.ts +410 -0
  13. package/src/commands/doctor/client/manifests.ts +161 -0
  14. package/src/commands/doctor/client/men.flow.ts +412 -0
  15. package/src/commands/doctor/client/ops.flow.ts +312 -0
  16. package/src/commands/doctor/client/options.ts +201 -0
  17. package/src/commands/doctor/client/ord.flow.ts +445 -0
  18. package/src/commands/doctor/client/order.ts +261 -0
  19. package/src/commands/doctor/client/pay.flow.ts +772 -0
  20. package/src/commands/doctor/client/repo.scan.ts +142 -0
  21. package/src/commands/doctor/client/runner.ts +182 -0
  22. package/src/commands/doctor/client/sdk.flow.ts +630 -0
  23. package/src/commands/doctor/client/store.flow.ts +577 -0
  24. package/src/commands/doctor/client/yaml.ts +179 -0
  25. package/src/commands/doctor/live/http.ts +81 -0
  26. package/src/commands/doctor/live/live.session.ts +250 -0
  27. package/src/commands/doctor/live/order.ts +95 -0
  28. package/src/commands/doctor/live/realtime.ts +161 -0
  29. package/src/commands/doctor/live/result.ts +76 -0
  30. package/src/commands/index.ts +8 -0
  31. package/src/core/command.types.ts +25 -0
  32. package/src/core/help.ts +85 -0
  33. package/src/core/i18n.ts +49 -0
  34. package/src/core/options.ts +131 -0
  35. package/src/core/output.ts +47 -0
  36. package/src/core/redact.ts +71 -0
  37. package/src/index.ts +4 -0
  38. package/src/main.ts +113 -0
  39. package/README.md +0 -3
@@ -0,0 +1,142 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+
4
+ const SKIP = new Set(["node_modules", ".git", "coverage"]);
5
+ const MAX_FILES = 2000;
6
+ const MAX_BYTES = 512000;
7
+
8
+ export interface SourceFile {
9
+ rel: string;
10
+ body: string;
11
+ }
12
+
13
+ const textName = (name: string) => {
14
+ return /\.(json|ts|tsx|js|jsx|mjs|cjs|md|html|css|txt)$/.test(name);
15
+ };
16
+
17
+ export const readSources = async (root: string) => {
18
+ const found: SourceFile[] = [];
19
+ const visit = async (dir: string) => {
20
+ if (found.length >= MAX_FILES) return;
21
+ let names: { name: string; isDirectory: () => boolean }[] = [];
22
+ try {
23
+ names = await readdir(dir, { withFileTypes: true });
24
+ } catch {
25
+ return;
26
+ }
27
+ for (const entry of names) {
28
+ if (found.length >= MAX_FILES) return;
29
+ if (SKIP.has(entry.name)) continue;
30
+ const path = join(dir, entry.name);
31
+ if (entry.isDirectory()) {
32
+ await visit(path);
33
+ continue;
34
+ }
35
+ if (!textName(entry.name)) continue;
36
+ let body = "";
37
+ try {
38
+ body = await readFile(path, "utf8");
39
+ } catch {
40
+ continue;
41
+ }
42
+ if (body.length > MAX_BYTES) continue;
43
+ found.push({ rel: relative(root, path), body });
44
+ }
45
+ };
46
+ await visit(root);
47
+ return found;
48
+ };
49
+
50
+ const HEADING = /^##\s+v?(\d+\.\d+\.\d+)\s*(?:[-–:]\s*(.*))?$/;
51
+
52
+ export interface ChangeNote {
53
+ version: string;
54
+ title: string;
55
+ description: string;
56
+ }
57
+
58
+ export const changelogNotes = (body: string) => {
59
+ const notes: ChangeNote[] = [];
60
+ let current: ChangeNote | null = null;
61
+ for (const line of body.split(/\r?\n/)) {
62
+ const match = HEADING.exec(line.trim());
63
+ if (match) {
64
+ if (current) notes.push(current);
65
+ const version = match[1] ?? "";
66
+ const title = match[2]?.trim() ?? "";
67
+ current = { version, title, description: "" };
68
+ continue;
69
+ }
70
+ if (!current || current.description || !line.trim()) continue;
71
+ current.description = line.trim();
72
+ }
73
+ if (current) notes.push(current);
74
+ return notes;
75
+ };
76
+
77
+ export const semverCmp = (left: string, right: string) => {
78
+ const a = left.split(".").map(part => Number(part));
79
+ const b = right.split(".").map(part => Number(part));
80
+ for (let i = 0; i < 3; i += 1) {
81
+ const av = a[i] ?? 0;
82
+ const bv = b[i] ?? 0;
83
+ if (Number.isNaN(av) || Number.isNaN(bv)) return 0;
84
+ if (av === bv) continue;
85
+ if (av > bv) return 1;
86
+ return -1;
87
+ }
88
+ return 0;
89
+ };
90
+
91
+ const SCOPES = ["dependencies", "devDependencies", "peerDependencies"];
92
+
93
+ const isRecord = (value: unknown): value is Record<string, unknown> => {
94
+ return !!value && typeof value === "object" && !Array.isArray(value);
95
+ };
96
+
97
+ export const workspaceIssues = (files: SourceFile[]) => {
98
+ const issues: string[] = [];
99
+ for (const file of files) {
100
+ if (!file.rel.endsWith("package.json")) continue;
101
+ let json: unknown;
102
+ try {
103
+ json = JSON.parse(file.body);
104
+ } catch {
105
+ continue;
106
+ }
107
+ if (!isRecord(json)) continue;
108
+ for (const scope of SCOPES) {
109
+ const bag = json[scope];
110
+ if (!isRecord(bag)) continue;
111
+ for (const [name, version] of Object.entries(bag)) {
112
+ if (!name.startsWith("@fire-spark/")) continue;
113
+ if (name === "@fire-spark/rn-ui") continue;
114
+ if (typeof version !== "string") continue;
115
+ if (!version.startsWith("workspace:")) continue;
116
+ issues.push(`${file.rel}: ${name}@${version}`);
117
+ }
118
+ }
119
+ }
120
+ return issues;
121
+ };
122
+
123
+ export const declaresDep = (
124
+ files: SourceFile[],
125
+ relPrefix: string,
126
+ name: string
127
+ ) => {
128
+ const docs = files.filter(file => file.rel.endsWith("package.json"));
129
+ for (const file of docs) {
130
+ const inApp = file.rel.startsWith(relPrefix);
131
+ const atRoot = file.rel === "package.json";
132
+ if (!inApp && !atRoot) continue;
133
+ if (file.body.includes(`"${name}"`)) return true;
134
+ }
135
+ return false;
136
+ };
137
+
138
+ export const importsLib = (body: string, lib: string) => {
139
+ if (body.includes(`"${lib}"`) || body.includes(`'${lib}'`)) return true;
140
+ if (body.includes(`"${lib}/`) || body.includes(`'${lib}/`)) return true;
141
+ return false;
142
+ };
@@ -0,0 +1,182 @@
1
+ import type { Locale } from "../../../core/i18n";
2
+ import { translate } from "../../../core/i18n";
3
+ import type { Output } from "../../../core/output";
4
+ import type { Redactor } from "../../../core/redact";
5
+ import { ADMIN_BINDINGS } from "./admin.flow";
6
+ import type { CatalogCheck } from "./catalog";
7
+ import { CHECKOUT_BINDINGS } from "./checkout.flow";
8
+ import { CONTENT_BINDINGS } from "./content.flow";
9
+ import { COPY } from "./copy";
10
+ import { LOG_BINDINGS } from "./log.flow";
11
+ import { listManifests, manifestProblem } from "./manifests";
12
+ import { parseLegacyManifest } from "./manifests";
13
+ import type { ManifestFile } from "./manifests";
14
+ import { MEN_BINDINGS } from "./men.flow";
15
+ import { OPS_BINDINGS } from "./ops.flow";
16
+ import { ORD_BINDINGS } from "./ord.flow";
17
+ import { ORDER_CHECK_ID, fixOrder, runOrder } from "./order";
18
+ import { PAY_BINDINGS } from "./pay.flow";
19
+ import { SDK_BINDINGS } from "./sdk.flow";
20
+ import { STORE_BINDINGS } from "./store.flow";
21
+ import type { YamlChannel } from "./yaml";
22
+
23
+ export interface Outcome {
24
+ status: "pass" | "fail" | "skip";
25
+ detail?: string;
26
+ }
27
+
28
+ export interface RunCtx {
29
+ locale: Locale;
30
+ cwd: string;
31
+ env: NodeJS.ProcessEnv;
32
+ channels: YamlChannel[];
33
+ channelId?: string;
34
+ named: ReadonlySet<string>;
35
+ redactor: Redactor;
36
+ }
37
+
38
+ export interface CheckRun {
39
+ id: string;
40
+ outcome: Outcome;
41
+ }
42
+
43
+ const duplicate = (
44
+ seen: Map<string, string>,
45
+ id: string,
46
+ file: string,
47
+ locale: RunCtx["locale"]
48
+ ): Outcome | null => {
49
+ const previous = seen.get(id);
50
+ if (!previous) {
51
+ seen.set(id, file);
52
+ return null;
53
+ }
54
+ const vars = { id, left: previous, right: file };
55
+ const detail = translate(locale, COPY.duplicateId, vars);
56
+ return { status: "fail", detail };
57
+ };
58
+
59
+ const idOf = (file: ManifestFile): Outcome | string | null => {
60
+ if (file.kind === "json") {
61
+ const problem = manifestProblem(file.body);
62
+ if (problem) return { status: "fail", detail: `${file.file}: ${problem}` };
63
+ const json = JSON.parse(file.body) as { channel_id?: unknown };
64
+ return typeof json.channel_id === "string" ? json.channel_id : null;
65
+ }
66
+ const parsed = parseLegacyManifest(file.body);
67
+ if (!parsed) {
68
+ const detail = `${file.file}: channel-manifest.ts is not a channel`;
69
+ return { status: "fail", detail };
70
+ }
71
+ return parsed.channelId;
72
+ };
73
+
74
+ const runSdk11 = async (ctx: RunCtx): Promise<Outcome> => {
75
+ const files = await listManifests(ctx.cwd);
76
+ if (files.length === 0) {
77
+ return { status: "pass", detail: translate(ctx.locale, COPY.noManifests) };
78
+ }
79
+ const seen = new Map<string, string>();
80
+ for (const file of files) {
81
+ const id = idOf(file);
82
+ if (id && typeof id !== "string") return id;
83
+ if (typeof id !== "string") continue;
84
+ const clash = duplicate(seen, id, file.file, ctx.locale);
85
+ if (clash) return clash;
86
+ }
87
+ return { status: "pass" };
88
+ };
89
+
90
+ const fixSdk11 = async (ctx: RunCtx): Promise<Outcome> => {
91
+ const result = await runSdk11(ctx);
92
+ if (result.status !== "fail") return result;
93
+ const note = translate(ctx.locale, COPY.noSafeFix);
94
+ const detail = result.detail ? `${note} ${result.detail}` : note;
95
+ return { status: "fail", detail };
96
+ };
97
+
98
+ /** Script or Maestro binding. Unlisted ids stay unbound until a flow exists. */
99
+ export interface CheckBinding {
100
+ kind: "script" | "maestro";
101
+ run: (ctx: RunCtx) => Promise<Outcome>;
102
+ fix?: (ctx: RunCtx) => Promise<Outcome>;
103
+ }
104
+
105
+ export const BINDINGS: Record<string, CheckBinding> = {
106
+ "SDK-11": { kind: "script", run: runSdk11, fix: fixSdk11 },
107
+ ...SDK_BINDINGS,
108
+ ...PAY_BINDINGS,
109
+ ...ORD_BINDINGS,
110
+ ...MEN_BINDINGS,
111
+ ...CHECKOUT_BINDINGS,
112
+ ...LOG_BINDINGS,
113
+ ...CONTENT_BINDINGS,
114
+ ...STORE_BINDINGS,
115
+ ...OPS_BINDINGS,
116
+ ...ADMIN_BINDINGS
117
+ };
118
+
119
+ export const runOne = async (
120
+ check: CatalogCheck,
121
+ ctx: RunCtx,
122
+ fix: boolean
123
+ ): Promise<Outcome> => {
124
+ if (check.id === "CLI-01") return { status: "pass" };
125
+ if (check.id === ORDER_CHECK_ID && !ctx.named.has(ORDER_CHECK_ID)) {
126
+ const detail = translate(ctx.locale, COPY.orderSkipped);
127
+ return { status: "skip", detail };
128
+ }
129
+ if (check.id === ORDER_CHECK_ID && fix) return fixOrder(ctx.locale);
130
+ if (check.id === ORDER_CHECK_ID) return runOrder(ctx);
131
+ const binding = BINDINGS[check.id];
132
+ if (!binding) {
133
+ return { status: "skip", detail: translate(ctx.locale, COPY.unbound) };
134
+ }
135
+ if (fix && check.fixable && binding.fix) return binding.fix(ctx);
136
+ return binding.run(ctx);
137
+ };
138
+
139
+ const jsonStatus = (status: Outcome["status"]) => {
140
+ if (status === "pass") return "passed";
141
+ if (status === "fail") return "failed";
142
+ return "skipped";
143
+ };
144
+
145
+ export const reportJson = (locale: Locale, runs: CheckRun[]) => {
146
+ const checks = runs.map(run => {
147
+ const status = jsonStatus(run.outcome.status);
148
+ if (run.outcome.status === "pass" && !run.outcome.detail) {
149
+ return { id: run.id, status };
150
+ }
151
+ return { id: run.id, status, reason: run.outcome.detail };
152
+ });
153
+ const passed = runs.filter(run => run.outcome.status === "pass").length;
154
+ const failed = runs.filter(run => run.outcome.status === "fail").length;
155
+ const skipped = runs.filter(run => run.outcome.status === "skip").length;
156
+ const totals = { passed, failed, skipped, total: runs.length };
157
+ return { locale, totals, groups: [{ id: "doctor", checks }] };
158
+ };
159
+
160
+ export const printRuns = (
161
+ output: Output,
162
+ locale: Locale,
163
+ runs: CheckRun[],
164
+ format: "text" | "json",
165
+ strict: boolean
166
+ ) => {
167
+ if (format === "json") {
168
+ output.line(JSON.stringify(reportJson(locale, runs), null, 2));
169
+ } else {
170
+ for (const run of runs) {
171
+ const detail = run.outcome.detail ? ` ${run.outcome.detail}` : "";
172
+ output.line(`${run.id} ${run.outcome.status}${detail}`);
173
+ }
174
+ }
175
+ const failed = runs.some(run => run.outcome.status === "fail");
176
+ const skipped = runs.some(run => run.outcome.status === "skip");
177
+ if (strict && skipped) {
178
+ output.error(translate(locale, COPY.strictSkipped));
179
+ }
180
+ if (failed || (strict && skipped)) return 1;
181
+ return 0;
182
+ };