@capuchoo/cli 0.2.1 → 0.4.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.
Files changed (79) hide show
  1. package/README.md +233 -26
  2. package/dist/base-command.d.ts +1 -1
  3. package/dist/base-command.js +1 -1
  4. package/dist/cli/prompts.d.ts +43 -0
  5. package/dist/cli/prompts.d.ts.map +1 -0
  6. package/dist/cli/prompts.js +137 -0
  7. package/dist/cli/prompts.js.map +1 -0
  8. package/dist/commands/app/delete.d.ts +20 -0
  9. package/dist/commands/app/delete.d.ts.map +1 -0
  10. package/dist/commands/app/delete.js +71 -0
  11. package/dist/commands/app/delete.js.map +1 -0
  12. package/dist/commands/app/list.d.ts +9 -0
  13. package/dist/commands/app/list.d.ts.map +1 -0
  14. package/dist/commands/app/list.js +40 -0
  15. package/dist/commands/app/list.js.map +1 -0
  16. package/dist/commands/auth/login.d.ts.map +1 -1
  17. package/dist/commands/auth/login.js +10 -13
  18. package/dist/commands/auth/login.js.map +1 -1
  19. package/dist/commands/auth/logout.js +2 -2
  20. package/dist/commands/auth/logout.js.map +1 -1
  21. package/dist/commands/auth/whoami.d.ts.map +1 -1
  22. package/dist/commands/auth/whoami.js +14 -2
  23. package/dist/commands/auth/whoami.js.map +1 -1
  24. package/dist/commands/channel/create.d.ts +25 -0
  25. package/dist/commands/channel/create.d.ts.map +1 -0
  26. package/dist/commands/channel/create.js +96 -0
  27. package/dist/commands/channel/create.js.map +1 -0
  28. package/dist/commands/channel/delete.d.ts +19 -0
  29. package/dist/commands/channel/delete.d.ts.map +1 -0
  30. package/dist/commands/channel/delete.js +66 -0
  31. package/dist/commands/channel/delete.js.map +1 -0
  32. package/dist/commands/channel/list.js +1 -1
  33. package/dist/commands/channel/list.js.map +1 -1
  34. package/dist/commands/doctor.d.ts +28 -0
  35. package/dist/commands/doctor.d.ts.map +1 -0
  36. package/dist/commands/doctor.js +278 -0
  37. package/dist/commands/doctor.js.map +1 -0
  38. package/dist/commands/init.d.ts +11 -0
  39. package/dist/commands/init.d.ts.map +1 -1
  40. package/dist/commands/init.js +151 -58
  41. package/dist/commands/init.js.map +1 -1
  42. package/dist/commands/org/create.d.ts +19 -0
  43. package/dist/commands/org/create.d.ts.map +1 -0
  44. package/dist/commands/org/create.js +45 -0
  45. package/dist/commands/org/create.js.map +1 -0
  46. package/dist/commands/org/list.d.ts +9 -0
  47. package/dist/commands/org/list.d.ts.map +1 -0
  48. package/dist/commands/org/list.js +41 -0
  49. package/dist/commands/org/list.js.map +1 -0
  50. package/dist/commands/setup.d.ts +16 -0
  51. package/dist/commands/setup.d.ts.map +1 -0
  52. package/dist/commands/setup.js +168 -0
  53. package/dist/commands/setup.js.map +1 -0
  54. package/dist/deploy/execute.d.ts.map +1 -1
  55. package/dist/deploy/execute.js +43 -30
  56. package/dist/deploy/execute.js.map +1 -1
  57. package/dist/pipeline/build.js +1 -1
  58. package/dist/pipeline/build.js.map +1 -1
  59. package/dist/pipeline/capacitor-support.d.ts +53 -0
  60. package/dist/pipeline/capacitor-support.d.ts.map +1 -0
  61. package/dist/pipeline/capacitor-support.js +95 -0
  62. package/dist/pipeline/capacitor-support.js.map +1 -0
  63. package/dist/pipeline/deploy.js +1 -1
  64. package/dist/pipeline/deploy.js.map +1 -1
  65. package/dist/pipeline/flavour.js +1 -1
  66. package/dist/pipeline/flavour.js.map +1 -1
  67. package/dist/services/cloud.d.ts +22 -2
  68. package/dist/services/cloud.d.ts.map +1 -1
  69. package/dist/services/cloud.js +26 -3
  70. package/dist/services/cloud.js.map +1 -1
  71. package/dist/utils/config.d.ts +1 -1
  72. package/dist/utils/config.js +3 -3
  73. package/dist/utils/config.js.map +1 -1
  74. package/dist/utils/http.d.ts +2 -0
  75. package/dist/utils/http.d.ts.map +1 -1
  76. package/dist/utils/http.js +4 -0
  77. package/dist/utils/http.js.map +1 -1
  78. package/oclif.manifest.json +352 -5
  79. package/package.json +13 -6
@@ -0,0 +1,278 @@
1
+ import { canPublishTo, hasEnvironmentMismatch, normaliseProjectConfig, suggestEnvironment, } from "@capuchoo/core";
2
+ import chalk from "chalk";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { BaseCommand } from "../base-command.js";
6
+ import { CloudClient } from "../services/cloud.js";
7
+ import { readProjectConfig, resolveCredentials } from "../utils/config.js";
8
+ /**
9
+ * Explains why a deploy will not work, before you run one.
10
+ *
11
+ * Every check here corresponds to something that has silently failed in this
12
+ * project: an endpoint pointing at a retired backend, a channel whose
13
+ * environment disagreed with its name, a channel serving nothing because the
14
+ * upload never moved its pointer, an app whose flavour env file did not exist.
15
+ * Each finding names the fix, because a diagnosis you cannot act on is just bad
16
+ * news.
17
+ */
18
+ export default class Doctor extends BaseCommand {
19
+ static description = "Check that this app, its credentials and its channels are usable";
20
+ static examples = ["<%= config.bin %> doctor"];
21
+ async run() {
22
+ const appDir = process.cwd();
23
+ const findings = [];
24
+ // --- credentials ---------------------------------------------------------
25
+ const credentials = resolveCredentials();
26
+ if (!credentials) {
27
+ this.report([{ level: "fail", what: "Not signed in", fix: "capuchoo auth login" }]);
28
+ return;
29
+ }
30
+ findings.push({ level: "ok", what: "Endpoint", detail: credentials.endpoint });
31
+ const cloud = new CloudClient(credentials.endpoint, credentials.apiKey);
32
+ const profile = await cloud.whoami().catch(() => null);
33
+ if (!profile) {
34
+ findings.push({
35
+ level: "fail",
36
+ what: "Credentials rejected",
37
+ detail: `${credentials.endpoint} did not accept the stored API key`,
38
+ // The endpoint is the likelier culprit: a retired backend answers
39
+ // /health but validates nothing.
40
+ fix: `capuchoo config set endpoint <url> (or capuchoo auth login)`,
41
+ });
42
+ this.report(findings);
43
+ return;
44
+ }
45
+ findings.push({ level: "ok", what: "Signed in", detail: profile.user.email });
46
+ // --- the link between this directory and a cloud app ---------------------
47
+ // Normalised, not raw: a v1 project.json carries only the cloud identifiers
48
+ // and every flavour is defaulted at runtime. Reading the raw fields reported
49
+ // "no flavour configured" for an app that deploys fine.
50
+ const raw = readProjectConfig(appDir);
51
+ const project = raw ? normaliseProjectConfig(raw) : null;
52
+ if (!project) {
53
+ findings.push({
54
+ level: "fail",
55
+ what: "This directory is not linked to an app",
56
+ fix: "capuchoo init",
57
+ });
58
+ this.report(findings);
59
+ return;
60
+ }
61
+ findings.push({
62
+ level: "ok",
63
+ what: "Linked",
64
+ detail: `${project.appName} (${project.appId})`,
65
+ });
66
+ // An app-scoped key lists every app the account owns and is refused only by
67
+ // the endpoints that publish - so without this check the first sign of a
68
+ // mismatch is a 403 at step 7 of 7, after a full build and zip.
69
+ if (!canPublishTo(profile, project.cloudAppId)) {
70
+ const scoped = profile.apps.find((app) => app.id === profile.credential?.app_id);
71
+ findings.push({
72
+ level: "fail",
73
+ what: "This API key cannot publish to this app",
74
+ detail: scoped
75
+ ? `The key is restricted to ${scoped.name} (${scoped.app_id})`
76
+ : `The key is restricted to app ${profile.credential?.app_id}`,
77
+ fix: "capuchoo auth login (with a key for this app, or an unscoped one)",
78
+ });
79
+ }
80
+ // --- channels ------------------------------------------------------------
81
+ const channels = await cloud.channels(project.cloudAppId).catch(() => null);
82
+ if (!channels) {
83
+ findings.push({
84
+ level: "fail",
85
+ what: "The linked cloud app could not be read",
86
+ detail: `cloudAppId ${project.cloudAppId} - deleted, or belongs to another account`,
87
+ fix: "capuchoo init --force",
88
+ });
89
+ this.report(findings);
90
+ return;
91
+ }
92
+ if (channels.length === 0) {
93
+ findings.push({
94
+ level: "fail",
95
+ what: "No channels",
96
+ detail: "Nothing can be deployed until a channel exists",
97
+ fix: "capuchoo channel create staging",
98
+ });
99
+ }
100
+ for (const channel of channels) {
101
+ if (!channel.environment) {
102
+ findings.push({
103
+ level: "fail",
104
+ what: `Channel "${channel.name}" has no environment`,
105
+ detail: "The environment is what tells the CLI which flavour to build",
106
+ fix: "Recreate it with capuchoo channel create, or set the environment " +
107
+ `in the dashboard (suggested: ${suggestEnvironment(channel.name) ?? "prod"})`,
108
+ });
109
+ continue;
110
+ }
111
+ if (hasEnvironmentMismatch(channel.name, channel.environment)) {
112
+ findings.push({
113
+ level: "warn",
114
+ what: `Channel "${channel.name}" is on the ${channel.environment} environment`,
115
+ detail: `Devices on it receive ${channel.environment} bundles, built from .env.${channel.environment}`,
116
+ fix: `Set it to ${suggestEnvironment(channel.name)} unless that is deliberate`,
117
+ });
118
+ }
119
+ // The pointer that decides what is served. An upload that does not move
120
+ // it leaves the channel answering "no update" forever.
121
+ if (!channel.current_version_id) {
122
+ findings.push({
123
+ level: "warn",
124
+ what: `Channel "${channel.name}" is serving no bundle`,
125
+ detail: "No OTA version is active on it yet",
126
+ fix: `capuchoo deploy ota --channel ${channel.name}`,
127
+ });
128
+ }
129
+ }
130
+ // --- flavours ------------------------------------------------------------
131
+ const environments = new Set(channels.map((channel) => channel.environment).filter(Boolean));
132
+ for (const environment of environments) {
133
+ const flavour = project.flavours[environment];
134
+ const envFile = path.join(appDir, flavour.envFile);
135
+ if (!fs.existsSync(envFile)) {
136
+ findings.push({
137
+ level: "fail",
138
+ what: `Missing ${flavour.envFile}`,
139
+ detail: `A ${environment} deploy reads it for the app id, version and update URL`,
140
+ fix: `Create ${flavour.envFile}`,
141
+ });
142
+ continue;
143
+ }
144
+ const contents = fs.readFileSync(envFile, "utf8");
145
+ const updateUrl = /^VITE_UPDATE_API_URL=(.*)$/m.exec(contents)?.[1]?.trim();
146
+ if (!updateUrl) {
147
+ findings.push({
148
+ level: "fail",
149
+ what: `${flavour.envFile} has no VITE_UPDATE_API_URL`,
150
+ detail: "The app would ship with updates silently disabled",
151
+ fix: `Add VITE_UPDATE_API_URL=${credentials.endpoint}`,
152
+ });
153
+ }
154
+ else if (updateUrl.replace(/\/+$/, "") !== credentials.endpoint.replace(/\/+$/, "")) {
155
+ // Not necessarily wrong - a custom domain in front of the same service
156
+ // is good practice - but it is the mistake that cost an afternoon.
157
+ findings.push({
158
+ level: "warn",
159
+ what: `${flavour.envFile} points at a different host`,
160
+ detail: `app: ${updateUrl} cli: ${credentials.endpoint}`,
161
+ fix: "Confirm both reach the same backend",
162
+ });
163
+ }
164
+ else {
165
+ findings.push({ level: "ok", what: `Flavour ${environment}`, detail: flavour.envFile });
166
+ }
167
+ }
168
+ // --- the app's own wiring ------------------------------------------------
169
+ const webDir = path.join(appDir, project.webDir ?? "dist");
170
+ if (!fs.existsSync(webDir)) {
171
+ findings.push({
172
+ level: "warn",
173
+ what: `webDir "${project.webDir}" does not exist yet`,
174
+ detail: "Fine before the first build; a deploy builds it",
175
+ });
176
+ }
177
+ findings.push(...this.checkRuntimeWiring(appDir));
178
+ this.report(findings);
179
+ }
180
+ /**
181
+ * Looks for the two calls that decide whether updates work at all.
182
+ *
183
+ * Textual, not semantic: this is a hint, not a compiler. Worth doing anyway -
184
+ * a missing `notifyAppReady()` rolls back working bundles ten seconds after
185
+ * they install, and that is invisible until it happens on a device.
186
+ */
187
+ checkRuntimeWiring(appDir) {
188
+ const findings = [];
189
+ const pkgPath = path.join(appDir, "package.json");
190
+ if (!fs.existsSync(pkgPath))
191
+ return findings;
192
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
193
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
194
+ if (!deps["@capuchoo/updater"]) {
195
+ findings.push({
196
+ level: "warn",
197
+ what: "@capuchoo/updater is not a dependency",
198
+ detail: "Deploys will work; the app will not check for what they publish",
199
+ fix: "pnpm add @capuchoo/updater",
200
+ });
201
+ return findings;
202
+ }
203
+ const sources = this.sourceFiles(path.join(appDir, "src"));
204
+ const joined = sources.map((file) => fs.readFileSync(file, "utf8")).join("\n");
205
+ if (!joined.includes("notifyAppReady")) {
206
+ findings.push({
207
+ level: "fail",
208
+ what: "notifyAppReady() is never called",
209
+ detail: "The plugin rolls back to the previous bundle when it does not hear this " +
210
+ "within its timeout - so every update would install and then revert",
211
+ fix: 'Call it early in main.ts: import { notifyAppReady } from "@capuchoo/updater"',
212
+ });
213
+ }
214
+ else {
215
+ findings.push({ level: "ok", what: "notifyAppReady() present" });
216
+ }
217
+ const capacitorConfig = ["capacitor.config.ts", "capacitor.config.js"]
218
+ .map((name) => path.join(appDir, name))
219
+ .find((file) => fs.existsSync(file));
220
+ if (capacitorConfig) {
221
+ const config = fs.readFileSync(capacitorConfig, "utf8");
222
+ if (!config.includes("capuchooUpdaterConfig") && !config.includes("capuchooUpdaterConfig")) {
223
+ findings.push({
224
+ level: "warn",
225
+ what: `${path.basename(capacitorConfig)} does not use capuchooUpdaterConfig()`,
226
+ detail: "Hand-written plugin config has shipped an empty updateUrl before, which " +
227
+ "disables updates without failing",
228
+ fix: "plugins.CapacitorUpdater = capuchooUpdaterConfig({ apiUrl, channel })",
229
+ });
230
+ }
231
+ else {
232
+ findings.push({ level: "ok", what: "Capacitor plugin config" });
233
+ }
234
+ }
235
+ return findings;
236
+ }
237
+ /** Shallow-ish walk: enough to find an import, cheap enough to run always. */
238
+ sourceFiles(dir, depth = 0) {
239
+ if (depth > 3 || !fs.existsSync(dir))
240
+ return [];
241
+ const found = [];
242
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
243
+ if (entry.name === "node_modules" || entry.name.startsWith("."))
244
+ continue;
245
+ const full = path.join(dir, entry.name);
246
+ if (entry.isDirectory())
247
+ found.push(...this.sourceFiles(full, depth + 1));
248
+ else if (/\.(ts|tsx|js|vue)$/.test(entry.name))
249
+ found.push(full);
250
+ }
251
+ return found;
252
+ }
253
+ report(findings) {
254
+ const mark = { ok: chalk.green("✓"), warn: chalk.yellow("!"), fail: chalk.red("✗") };
255
+ this.log("");
256
+ for (const finding of findings) {
257
+ this.log(` ${mark[finding.level]} ${finding.what}`);
258
+ if (finding.detail)
259
+ this.log(chalk.dim(` ${finding.detail}`));
260
+ if (finding.fix)
261
+ this.log(chalk.cyan(` → ${finding.fix}`));
262
+ }
263
+ const failed = findings.filter((f) => f.level === "fail").length;
264
+ const warned = findings.filter((f) => f.level === "warn").length;
265
+ this.log("");
266
+ if (failed === 0 && warned === 0) {
267
+ this.log(chalk.green(" Ready to deploy."));
268
+ }
269
+ else {
270
+ this.log(` ${failed} blocking, ${warned} worth a look. ` +
271
+ chalk.dim("Fix the blocking ones before deploying."));
272
+ }
273
+ this.log("");
274
+ if (failed > 0)
275
+ process.exitCode = 1;
276
+ }
277
+ }
278
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/commands/doctor.ts"],"names":[],"mappings":"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"}
@@ -4,9 +4,20 @@ export default class Init extends BaseCommand {
4
4
  static examples: string[];
5
5
  static flags: {
6
6
  link: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ create: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ "app-id": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ org: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ channel: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
12
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
13
  };
9
14
  run(): Promise<void>;
15
+ /**
16
+ * Creates the first channel, because a linked app without one cannot be
17
+ * deployed to. The environment is chosen explicitly: a channel's name never
18
+ * decides which flavour it serves.
19
+ */
20
+ private offerFirstChannel;
10
21
  /** Detects which flavours the project actually has files for. */
11
22
  private detectFlavours;
12
23
  /** Reads `webDir` out of capacitor.config.* so the CLI zips the right folder. */
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,CAAC,OAAO,OAAO,IAAK,SAAQ,WAAW;IAC3C,OAAgB,WAAW,SAC8C;IAEzE,OAAgB,QAAQ,WAA+D;IAEvF,OAAgB,KAAK;;;MAWnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAwI1B,iEAAiE;IACjE,OAAO,CAAC,cAAc;IAatB,iFAAiF;IACjF,OAAO,CAAC,YAAY;YAYN,YAAY;YAqBZ,SAAS;CAiExB"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AASjD,MAAM,CAAC,OAAO,OAAO,IAAK,SAAQ,WAAW;IAC3C,OAAgB,WAAW,SACgD;IAE3E,OAAgB,QAAQ,WAKtB;IAEF,OAAgB,KAAK;;;;;;;;MAqCnB;IAEI,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA8J1B;;;;OAIG;YACW,iBAAiB;IAmE/B,iEAAiE;IACjE,OAAO,CAAC,cAAc;IAatB,iFAAiF;IACjF,OAAO,CAAC,YAAY;YAYN,YAAY;YA2BZ,SAAS;CAoFxB"}
@@ -1,5 +1,5 @@
1
- import { confirm, input, select } from "@inquirer/prompts";
2
- import { ENVIRONMENTS, PROJECT_CONFIG_VERSION, canCreateApps, defaultFlavour, isValidBundleId, } from "@capuchoo/core";
1
+ import { askText, confirm, isInteractive, log, selectOne } from "../cli/prompts.js";
2
+ import { ENVIRONMENTS, PROJECT_CONFIG_VERSION, canCreateApps, canPublishTo, defaultFlavour, environmentMismatchWarning, suggestEnvironment, isValidBundleId, } from "@capuchoo/core";
3
3
  import { Flags } from "@oclif/core";
4
4
  import chalk from "chalk";
5
5
  import fs from "node:fs";
@@ -10,13 +10,44 @@ import { projectConfigPath, readProjectConfig, resolveCredentials, writeProjectC
10
10
  import AuthLogin from "./auth/login.js";
11
11
  import { BaseCommand } from "../base-command.js";
12
12
  export default class Init extends BaseCommand {
13
- static description = "Link this directory to a Capucho app and write .capucho/project.json";
14
- static examples = ["<%= config.bin %> init", "<%= config.bin %> init --link"];
13
+ static description = "Link this directory to a Capuchoo app and write .capuchoo/project.json";
14
+ static examples = [
15
+ "<%= config.bin %> init",
16
+ "<%= config.bin %> init --link",
17
+ "<%= config.bin %> init --create",
18
+ '<%= config.bin %> init --create --name "My App" --app-id com.acme.app --channel staging',
19
+ ];
15
20
  static flags = {
16
21
  link: Flags.boolean({
17
22
  char: "l",
18
23
  default: false,
19
- description: "Link an existing app instead of creating one",
24
+ description: "Link an existing app instead of asking",
25
+ exclusive: ["create"],
26
+ }),
27
+ // Without this, a non-interactive shell could only ever link: the prompt
28
+ // named --link as its escape hatch and there was no flag for the other half
29
+ // of the same question.
30
+ create: Flags.boolean({
31
+ char: "c",
32
+ default: false,
33
+ description: "Create a new app instead of asking",
34
+ exclusive: ["link"],
35
+ }),
36
+ name: Flags.string({
37
+ description: "Name of the app to create (default: this directory's name)",
38
+ dependsOn: ["create"],
39
+ }),
40
+ // Used by both halves: with --create it is the identifier to register, with
41
+ // --link the identifier to select. Either way it names the app.
42
+ "app-id": Flags.string({
43
+ description: "Bundle identifier of the app, e.g. com.company.app",
44
+ }),
45
+ org: Flags.string({
46
+ description: "Organization to create the app in, by name or id",
47
+ dependsOn: ["create"],
48
+ }),
49
+ channel: Flags.string({
50
+ description: "Create this channel after linking, e.g. staging",
20
51
  }),
21
52
  force: Flags.boolean({
22
53
  char: "f",
@@ -28,19 +59,16 @@ export default class Init extends BaseCommand {
28
59
  const { flags } = await this.parse(Init);
29
60
  const appDir = process.cwd();
30
61
  this.log("");
31
- this.log(chalk.bold(" Initialise Capucho"));
62
+ this.log(chalk.bold(" Initialise Capuchoo"));
32
63
  this.log(chalk.dim(` ${appDir}`));
33
64
  this.log("");
34
65
  const existing = readProjectConfig(appDir);
35
66
  if (existing && !flags.force) {
36
67
  this.log(chalk.yellow(" Already initialised: ") + `${existing.appName} (${existing.appId})`);
37
- const action = await select({
38
- message: "What now?",
39
- choices: [
40
- { name: "Keep it", value: "keep" },
41
- { name: "Re-link to a different app", value: "relink" },
42
- ],
43
- });
68
+ const action = await selectOne("What now?", [
69
+ { value: "keep", label: "Keep it", hint: existing.appId },
70
+ { value: "relink", label: "Re-link to a different app" },
71
+ ], "--force");
44
72
  if (action === "keep")
45
73
  return;
46
74
  }
@@ -67,14 +95,28 @@ export default class Init extends BaseCommand {
67
95
  // --- pick or create the app ---------------------------------------------
68
96
  const mode = flags.link
69
97
  ? "existing"
70
- : await select({
71
- message: "Link this directory to",
72
- choices: [
73
- { name: "An app that already exists", value: "existing" },
74
- { name: "A new app", value: "new" },
75
- ],
98
+ : flags.create
99
+ ? "new"
100
+ : await selectOne("Should this directory publish to an existing app, or a new one?", [
101
+ { value: "existing", label: "An app that already exists" },
102
+ { value: "new", label: "A new app" },
103
+ ], "--link or --create");
104
+ const app = mode === "existing"
105
+ ? await this.linkExisting(cloud, flags["app-id"])
106
+ : await this.createNew(cloud, {
107
+ name: flags.name,
108
+ appId: flags["app-id"],
109
+ org: flags.org,
76
110
  });
77
- const app = mode === "existing" ? await this.linkExisting(cloud) : await this.createNew(cloud);
111
+ // The key that just created or linked this app may not be allowed to publish
112
+ // to it: an app-scoped key can read every app the account owns. Say so here
113
+ // rather than let the first deploy discover it after a full build.
114
+ if (!canPublishTo(profile, app.id)) {
115
+ this.log("");
116
+ this.log(chalk.yellow(" The API key in use is restricted to another app, so deploys from here\n" +
117
+ ` will be refused. Run "capuchoo auth login" with a key for ${app.name}\n` +
118
+ " or an unscoped one."));
119
+ }
78
120
  // --- flavours ------------------------------------------------------------
79
121
  const flavours = this.detectFlavours(appDir);
80
122
  const detected = Object.keys(flavours);
@@ -127,12 +169,55 @@ export default class Init extends BaseCommand {
127
169
  chalk.cyan(`capuchoo deploy ota --channel ${deployable[0].name}`));
128
170
  }
129
171
  else {
130
- this.log(chalk.yellow(" This app has no channel with an environment set yet."));
131
- this.log(chalk.dim(" Create one in the dashboard - the environment is what tells the CLI\n" +
132
- " which flavour to build."));
172
+ // An app with no channel is not deployable, and init already knows it.
173
+ // Sending people to the dashboard here was the one thing that made setting
174
+ // an app up a CLI-browser-CLI round trip.
175
+ await this.offerFirstChannel(cloud, app, flags.channel);
133
176
  }
134
177
  this.log("");
135
178
  }
179
+ /**
180
+ * Creates the first channel, because a linked app without one cannot be
181
+ * deployed to. The environment is chosen explicitly: a channel's name never
182
+ * decides which flavour it serves.
183
+ */
184
+ async offerFirstChannel(cloud, app, requested) {
185
+ this.log(chalk.yellow(" This app has no channel yet, so nothing can be deployed to it."));
186
+ this.log(chalk.dim(" A channel's environment is what tells the CLI which flavour to build.\n"));
187
+ const name = requested?.trim();
188
+ if (!name && !isInteractive()) {
189
+ this.log(chalk.dim(" Create one with: ") +
190
+ chalk.cyan("capuchoo channel create staging") +
191
+ chalk.dim(" (or pass --channel)"));
192
+ return;
193
+ }
194
+ if (!name) {
195
+ const create = await confirm("Create one now?", { default: true });
196
+ if (!create) {
197
+ this.log(chalk.dim(" Later: ") + chalk.cyan("capuchoo channel create staging"));
198
+ return;
199
+ }
200
+ }
201
+ const chosen = name ?? (await askText("Channel name", { initial: "staging", flag: "--channel" })).trim();
202
+ const environment = suggestEnvironment(chosen) ??
203
+ (await selectOne(`Which flavour should "${chosen}" serve?`, ENVIRONMENTS.map((value) => ({ value, label: value })), "--channel <name> with a name like staging"));
204
+ const warning = environmentMismatchWarning(chosen, environment);
205
+ if (warning)
206
+ log.warn(warning);
207
+ try {
208
+ const channel = await cloud.createChannel({ app_id: app.id, name: chosen, environment });
209
+ this.log("");
210
+ this.log(` ${chalk.green("Created channel")} ${channel.name} ${chalk.dim(`(${environment})`)}`);
211
+ this.log(chalk.dim(" Deploy with: ") + chalk.cyan(`capuchoo deploy ota --channel ${channel.name}`));
212
+ }
213
+ catch (error) {
214
+ // The app and project.json are already correct, so this is a warning and
215
+ // not a failure - `capuchoo channel create` retries just this step.
216
+ this.log("");
217
+ this.log(chalk.yellow(` The channel could not be created: ${error instanceof Error ? error.message : String(error)}`));
218
+ this.log(chalk.dim(` Retry with: capuchoo channel create ${chosen}`));
219
+ }
220
+ }
136
221
  /** Detects which flavours the project actually has files for. */
137
222
  detectFlavours(appDir) {
138
223
  const found = {};
@@ -156,23 +241,24 @@ export default class Init extends BaseCommand {
156
241
  }
157
242
  return "dist";
158
243
  }
159
- async linkExisting(cloud) {
244
+ async linkExisting(cloud, wanted) {
160
245
  const spinner = ora({ text: "Fetching apps", stream: process.stderr }).start();
161
246
  const apps = await cloud.apps();
162
247
  spinner.stop();
163
248
  if (apps.length === 0) {
164
- this.error("This account has no apps yet. Re-run and choose to create one, or " +
165
- "create it in the dashboard.");
166
- }
167
- return select({
168
- message: "App",
169
- choices: apps.map((app) => ({
170
- name: `${app.name} ${chalk.dim(app.app_id)}`,
171
- value: app,
172
- })),
173
- });
249
+ this.error("This account has no apps yet. Run capuchoo init --create to register one.");
250
+ }
251
+ if (wanted) {
252
+ const match = apps.find((app) => app.app_id === wanted || app.id === wanted);
253
+ if (!match) {
254
+ this.error(`No app called "${wanted}" on this account. ` +
255
+ `Available: ${apps.map((app) => app.app_id).join(", ")}.`);
256
+ }
257
+ return match;
258
+ }
259
+ return selectOne("Which app does this directory publish to?", apps.map((app) => ({ value: app, label: app.name, hint: app.app_id })), "--app-id");
174
260
  }
175
- async createNew(cloud) {
261
+ async createNew(cloud, given) {
176
262
  const spinner = ora({
177
263
  text: "Fetching organizations",
178
264
  stream: process.stderr,
@@ -186,30 +272,37 @@ export default class Init extends BaseCommand {
186
272
  : "This account is a member, not an owner or admin, of every organization " +
187
273
  "it belongs to, so it cannot create an app. Ask an owner.");
188
274
  }
189
- const organizationId = allowed.length === 1
190
- ? allowed[0].id
191
- : await select({
192
- message: "Organization",
193
- choices: allowed.map((org) => ({
194
- name: `${org.name} ${chalk.dim(`(${org.role})`)}`,
195
- value: org.id,
196
- })),
197
- });
198
- const name = await input({
199
- message: "App name",
200
- default: path.basename(process.cwd()),
201
- validate: (value) => value.trim().length > 0 || "Required",
202
- });
203
- const appId = await input({
204
- message: "Production bundle identifier",
205
- validate: (value) => isValidBundleId(value.trim()) ||
206
- "Expected something like com.company.app - lower case, at least two segments",
207
- });
275
+ const requested = given.org?.trim().toLowerCase();
276
+ const chosen = requested
277
+ ? allowed.find((org) => org.id === given.org?.trim() || org.name.toLowerCase() === requested)
278
+ : undefined;
279
+ if (requested && !chosen) {
280
+ this.error(`No organization called "${given.org}" that this account can create apps in. ` +
281
+ `Available: ${allowed.map((org) => org.name).join(", ")}.`);
282
+ }
283
+ const organizationId = chosen?.id ??
284
+ (allowed.length === 1
285
+ ? allowed[0].id
286
+ : await selectOne("Organization", allowed.map((org) => ({ value: org.id, label: org.name, hint: org.role })), "--org"));
287
+ const name = given.name?.trim() ||
288
+ (await askText("App name", {
289
+ initial: path.basename(process.cwd()),
290
+ flag: "--name",
291
+ }));
292
+ const appId = given.appId?.trim() ||
293
+ (await askText("Production bundle identifier", {
294
+ placeholder: "com.company.app",
295
+ flag: "--app-id",
296
+ validate: (value) => isValidBundleId(value.trim())
297
+ ? undefined
298
+ : "Expected something like com.company.app - lower case, at least two segments",
299
+ }));
300
+ if (!isValidBundleId(appId)) {
301
+ this.error(`"${appId}" is not a bundle identifier. Expected something like com.company.app - ` +
302
+ "lower case, at least two segments.");
303
+ }
208
304
  // Creating an app is the one irreversible thing this command does.
209
- const proceed = await confirm({
210
- message: `Create "${name}" (${appId})?`,
211
- default: true,
212
- });
305
+ const proceed = await confirm(`Create "${name}" (${appId})?`, { default: true });
213
306
  if (!proceed)
214
307
  this.error("Cancelled.");
215
308
  const creating = ora({ text: "Creating app", stream: process.stderr }).start();